Azure Functions, how to have multiple .json config files

Viewed 4904

So I have written an azure function that works locally just fine. I believe this is down to having the local.setting.json file. But when I publish it to azure the function does not work as it can't find the settings values that I had defined. Coming from a web app and console driven approach, we would have different config files that would be associated with each environment. How can I get this to work so I can have multiple settings.json files, e.g. one for dev, stag and prod environments? The end result is to get this deployed with octopus deploy, but at this point, if I cant even get it working with a publish then there is no chance of doing this.

I'm rather confused why this information is not easily available, as would presume it is a common thing to do?

3 Answers

I would love to see functions support environment specific settings in the same way as asp.net core or console apps. In the meantime I am using below code, which is somewhat hacky (see comments).

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        // Get the path to the folder that has appsettings.json and other files.
        // Note that there is a better way to get this path: ExecutionContext.FunctionAppDirectory when running inside a function. But we don't have access to the ExecutionContext here.
        // Functions team should improve this in future. It will hopefully expose FunctionAppDirectory through some other way or env variable.
        string basePath = IsDevelopmentEnvironment() ?
            Environment.GetEnvironmentVariable("AzureWebJobsScriptRoot") :
            $"{Environment.GetEnvironmentVariable("HOME")}\\site\\wwwroot";

        var config = new ConfigurationBuilder()
            .SetBasePath(basePath)
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)  // common settings go here.
            .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("AZURE_FUNCTIONS_ENVIRONMENT")}.json", optional: false, reloadOnChange: false)  // environment specific settings go here
            .AddJsonFile("local.settings.json", optional: true, reloadOnChange: false)  // secrets go here. This file is excluded from source control.
            .AddEnvironmentVariables()
            .Build();

        builder.Services.AddSingleton<IConfiguration>(config);
    }

    public bool IsDevelopmentEnvironment()
    {
        return "Development".Equals(Environment.GetEnvironmentVariable("AZURE_FUNCTIONS_ENVIRONMENT"), StringComparison.OrdinalIgnoreCase);
    }
}

OK so I have it working now :) Because we use octopus deploy we don't want multiple config files so we just have the one appsettings.Release.json file which gets the values substituted base on the environment being deployed too.

Below is the main function code.

public static class Function
    {
        // Format in a CRON Expression e.g. {second} {minute} {hour} {day} {month} {day-of-week}
        // https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-timer
        // [TimerTrigger("0 59 23 * * *") = 11:59pm
        [FunctionName("Function")]
        public static void Run([TimerTrigger("0 59 23 * * *")]TimerInfo myTimer, ILogger log)
        {

            // If running in debug then we dont want to load the appsettings.json file, this has its variables substituted in octopus
            // Running locally will use the local.settings.json file instead
#if DEBUG
            IConfiguration config = new ConfigurationBuilder()
                .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
                .AddEnvironmentVariables()
                .Build();
#else
            IConfiguration config = Utils.GetSettingsFromReleaseFile();
#endif

            // Initialise dependency injections
            var serviceProvider = Bootstrap.ConfigureServices(log4Net, config);

            var retryCount = Convert.ToInt32(config["RetryCount"]);

            int count = 0;
            while (count < retryCount)
            {
                count++;
                try
                {
                    var business = serviceProvider.GetService<IBusiness>();
                    business.UpdateStatusAndLiability();
                    return;
                }
                catch (Exception e)
                {
                    // Log your error
                }
            }

        }

    }

The Utils.cs file looks as follows

public static class Utils
    {

        public static string LoadSettingsFromFile(string environmentName)
        {
            var executableLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            // We need to go back up one level as the appseetings.Release.json file is not put in the bin directory
            var actualPathToConfig = Path.Combine(executableLocation, $"..\\appsettings.{environmentName}.json");
            using (StreamReader reader = new StreamReader(actualPathToConfig))
            {
                return reader.ReadToEnd();
            }
        }

        public static IConfiguration GetSettingsFromReleaseFile()
        {
            var json = Utils.LoadSettingsFromFile("Release");
            var memoryFileProvider = new InMemoryFileProvider(json);

            var config = new ConfigurationBuilder()
                .AddJsonFile(memoryFileProvider, "appsettings.json", false, false)
                .Build();
            return config;
        }

    }

The appsettings.Release.json is set as Content and Copy Always in visual studio. It looks like this

{
  "RetryCount": "#{WagonStatusAndLiabilityRetryCount}",
  "RetryWaitInSeconds": "#{WagonStatusAndLiabilityRetryWaitInSeconds}",
  "DefaultConnection": "#{YourConnectionString}"
}

Actually I believe you could have an appsettings.config file there already and skip the appsettings.Release.json file, but this is working and you can do what you want with it now.

This doc has the description about local.settings.json:

By default, these settings are not migrated automatically when the project is published to Azure.

One way is using --publish-local-settings:

Publish settings in local.settings.json to Azure, prompting to overwrite if the setting already exists.

Another way is to use the Manage Application Settings, the Remote is the current setting in the function app in Azure. Or choose Add setting to create a new app setting. For details you could refer to this doc:Function app settings.

enter image description here

Related