Since you've landed on this article, you must have experienced some of the confusion tied to not committing the local.settings.json file to source control. It's not entirely obvious how developers are supposed to manage the local application settings for their Azure Functions. We can all agree that we do not wish to store any application secrets in source control. From that perspective, excluding the local.settings.json file from source control is a no-brainer.

As mentioned in my other articles regarding Azure Functions, the Azure Functions project template excludes the local.settings.json file from source control by default. If you open up the .gitignore, there is a line to exclude the local.settings.json file near the top.

# Azure Functions localsettings file
local.settings.json

If you couldn't tell by the name, Microsoft has intended for this local.settings.json file to be for local development purposes only. It is not intended to be committed to a source control repository and it is not intended to be deployed to an Azure environment.

Omitting the settings from source control can be problematic from the perspective of working on a development team. When a new developer pulls down the repository, she will first have to obtain someone else's local.settings.json before she can run and debug the application. This is a painful blocking point for a dev team. We don't want to communicate any more than is absolutely necessary.

Even for those working on an Azure Functions project alone, having no traceable record of what the environmental settings are for an application will cause some headaches. Perhaps not at first, but when you put this project on the backburner for six months and then decide to wipe the dust off, you'll likely be frustrated with the lack of local environment variables. "Where are my app settings? How the heck do I run this application?"

The Solution

Let's take a look at an example function that logs application settings.

[FunctionName("Function1")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
    ILogger log,
    ExecutionContext context)
{
    var config = new ConfigurationBuilder()
        .SetBasePath(context.FunctionAppDirectory)
        .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
        .AddEnvironmentVariables()
        .Build();

    var myString = config["MyCustomStringSetting"];
    var myNumber = config.GetValue<int>("MyCustomNumberSetting");
    var mailSettings = new MailSettings();
    config.Bind("MailSettings", mailSettings);


    log.LogInformation($"MyCustomStringSetting: {myString}");
    log.LogInformation($"MyCustomNumberSetting: {myNumber}");
    log.LogInformation($"MailSettings: {JsonConvert.SerializeObject(mailSettings)}");

    return new OkResult();
}

Here's the example local.settings.json with secrets that are being read by the above function.

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet"
  },
  "ConnectionStrings": {
    "SqlConnectionString": "server=myddatabaseserver;user=tom;passsword=123;"
  },
  "MyCustomStringSetting": "Some Name",
  "MyCustomNumberSetting": 123,
  "MailSettings": {
    "FromAddress": "[email protected]",
    "ToAddress": "[email protected]",
    "MailServer": "smtp.mymailserver.com",
    "PrivateKey": "xYasdf5678asjifSDFGhasn1234sDGFHg"
  }
}

For the sake of completeness, here's the MailSettings POCO class.

public class MailSettings
{
    public string ToAddress { get; set; }

    public string FromAddress { get; set; }

    public string MailServer { get; set; }

    public string PrivateKey { get; set; }
}

Now, the simplest solution to the problem described above is to remove local.settings.json from the .gitignore, then carefully omit any secrets from the settings file before committing. In this case, at the very least, we'd want to omit the values for our connection string and the "PrivateKey" for our mail server. This strategy is dangerous. It's inevitable that you will, one day, be in a rush to get your changes committed. In the midst of your panicked mouse clicks, you will accidentally commit and push your precious secrets to the remote repository. Oops!

Thus, it is obviously not advised that you attempt to manually manage the omission of your secret environment variables. You will definitely get burned.

The solution I've settled on, for now, is to add one more configuration file called secret.settings.json.

{
  "ConnectionStrings": {
    "SqlConnectionString": "server=myddatabaseserver;user=tom;password=123;"
  },
  "MyCustomStringSetting": "Override Some Name",
  "MailSettings": {
    "PrivateKey": "xYasdf5678asjifSDFGhasn1234sDGFHg"
  }
}

Here is the associated local.settings.json with all secrets omitted.

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet"
  },
  "ConnectionStrings": {
    "SqlConnectionString": "--SECRET--"
  },
  "MyCustomStringSetting": "Some Name",
  "MyCustomNumberSetting": 123,
  "MailSettings": {
    "FromAddress": "[email protected]",
    "ToAddress": "[email protected]",
    "MailServer": "smtp.mymailserver.com",
    "PrivateKey": "--SECRET--"
  }
}

This is the example function that reads the new secret.settings.json file.

[FunctionName("Function1")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
    ILogger log,
    ExecutionContext context)
{
    var config = new ConfigurationBuilder()
        .SetBasePath(context.FunctionAppDirectory)
        .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
        .AddJsonFile("secret.settings.json", optional: true, reloadOnChange: true)
        .AddEnvironmentVariables()
        .Build();

    var myString = config["MyCustomStringSetting"];
    var myNumber = config.GetValue<int>("MyCustomNumberSetting");
    var mailSettings = new MailSettings();
    config.Bind("MailSettings", mailSettings);


    log.LogInformation($"MyCustomStringSetting: {myString}");
    log.LogInformation($"MyCustomNumberSetting: {myNumber}");
    log.LogInformation($"MailSettings: {JsonConvert.SerializeObject(mailSettings)}");

    return new OkResult();
}

We've added a line (AddJsonFile("secret.settings.json", optional:true, reloadOnChange: true)) to the config builder to optionally add our new JSON configuration file. The order of these builder methods is important, as each subsequent configuration source potentially overrides the application settings from its predecessors. First the local.settings.json settings are applied, then our secrets and local overrides from secrect.settings.json, and finally the environment variables.

We can observe the overriding behavior in the console output.

[10/23/2018 5:53:11 PM] MyCustomStringSetting: Override Some Name
[10/23/2018 5:53:11 PM] MyCustomNumberSetting: 123
[10/23/2018 5:53:11 PM] MailSettings: {"ToAddress":"[email protected]","FromAddress":"[email protected]","MailServer":"smtp.mymailserver.com","PrivateKey":"xYasdf5678asjifSDFGhasn1234sDGFHg"}

Remember to exchange the local.settings.json line in the .gitignore file for secret.settings.json. This will keep our secrets out of source control and allow us to check in all of the settings in the local.settings.json.

Advantages of Additional Config File

What I like about the addition of the secret.settings.json is that we are now able to safely add all of the settings that our functions are dependent on to source control. Any secret values can simply be redacted from the local.settings.json, which is now freely shared among developers and committed to source control.

We won't find ourselves having to reverse-engineer the local.settings.json file a year from now because the last known developer with a local copy was hit by a bus. Relevant application settings are documented and changes made to them are properly tracked.

Environment Variables in Azure

Any environment settings or connection strings specified in the local.settings.json must be specified in Azure. This can be done by updating application settings in Azure Portal or by creating and deploying ARM templates. If you're interested, you can sift through the docs for ARM templates and how to manage ARM templates with Visual Studio.

Azure Key Vault

Another notable solution is to place your secrets in Azure Key Vault. I would highly suggest doing this for any serious projects. There is a minor cost associated with the Azure Key Vault service, but setup is simple. The benefit is that you have your secrets managed in a secure, central location. This is preferable to having your secrets tossed around in emails and local file systems.

Using the Azure Key Vault service will still require your application to know at least one secret in order to access the keys, so the additional configuration file strategy described earlier still applies.

Here’s a sample of what that might look like in your Azure Function.

var configBuilder = new ConfigurationBuilder()
    .SetBasePath(context.FunctionAppDirectory)
    .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
    .AddJsonFile("secret.settings.json", optional: true, reloadOnChange: true)
    .AddEnvironmentVariables();

var config = configBuilder.Build();

configBuilder.AddAzureKeyVault(
    $"https://{config["AzureKeyVault:VaultName"]}.vault.azure.net/",
    config["AzureKeyVault:ClientId"],
    config["AzureKeyVault:ClientSecret"]
);

config = configBuilder.Build();

Recap

To recap, you can add more configuration files to separate your secrets, then you can at least add your non-secrets to source control. You obviously do not need to name your additional file secrets.settings.json. Choose whatever name you want. In fact, if you have enough secrets, it may make sense for you to organize them across several additional config files.

I hope this has been helpful for you. Leave a comment below if you have any questions or have come across a better way to manage secrets.