www.tomfaltesek.com Open in urlscan Pro
2606:4700:3032::6815:5805  Public Scan

URL: https://www.tomfaltesek.com/azure-functions-local-settings-json-and-source-control/
Submission: On September 12 via api from US — Scanned from DE

Form analysis 1 forms found in the DOM

<form id="contact-form" class="contact-form">
  <p id="contact-error" class="contact-error"> An error occured. Failed to send message. </p>
  <div class="g-recaptcha" data-size="invisible" data-sitekey="6LdTsHIUAAAAAF_97o4PbFuzPwMJbbIq73IxWoNU" data-callback="submitContactForm">
    <div class="grecaptcha-badge" data-style="bottomright"
      style="width: 256px; height: 60px; display: block; transition: right 0.3s ease 0s; position: fixed; bottom: 14px; right: -186px; box-shadow: gray 0px 0px 5px; border-radius: 2px; overflow: hidden;">
      <div class="grecaptcha-logo"><iframe title="reCAPTCHA"
          src="https://www.google.com/recaptcha/api2/anchor?ar=1&amp;k=6LdTsHIUAAAAAF_97o4PbFuzPwMJbbIq73IxWoNU&amp;co=aHR0cHM6Ly93d3cudG9tZmFsdGVzZWsuY29tOjQ0Mw..&amp;hl=de&amp;v=0hCdE87LyjzAkFO5Ff-v7Hj1&amp;size=invisible&amp;cb=a62iikiugvql"
          width="256" height="60" role="presentation" name="a-qe80iy4jllqm" frameborder="0" scrolling="no" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-top-navigation allow-modals allow-popups-to-escape-sandbox"></iframe>
      </div>
      <div class="grecaptcha-error"></div><textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response"
        style="width: 250px; height: 40px; border: 1px solid rgb(193, 193, 193); margin: 10px 25px; padding: 0px; resize: none; display: none;"></textarea>
    </div><iframe style="display: none;"></iframe>
  </div>
  <div class="form-group">
    <input class="form-input" type="text" name="name" placeholder="Name" required="">
  </div>
  <div class="form-group">
    <input class="form-input" type="email" name="email" placeholder="youremail@example.com" required="">
  </div>
  <div class="form-group">
    <textarea class="form-input" name="message" placeholder="Hi Tom. I have a great project for you..." required="" maxlength="3000"></textarea>
  </div>
  <button id="contact-send-button" class="send-button" type="submit">
    <span>Send</span>
    <div id="contact-spinner" class="spinner-ring">
      <div></div>
      <div></div>
      <div></div>
      <div></div>
    </div>
  </button>
</form>

Text Content

Hire Tom

23 October 2018 / Azure Functions


AZURE FUNCTIONS LOCAL.SETTINGS.JSON SECRETS AND SOURCE CONTROL

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": "testing123@email.com",
    "ToAddress": "receiver@email.com",
    "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": "local-testing123@email.com",
    "ToAddress": "receiver@email.com",
    "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":"receiver@email.com","FromAddress":"local-testing123@email.com","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.

TOM FALTESEK

Read more posts by this author.

Read More


Please enable JavaScript to view the comments powered by Disqus.
— Tom Faltesek —


AZURE FUNCTIONS


 * Azure Functions reCAPTCHA Integration
 * Azure Functions Input Validation with FluentValidation
 * Azure Functions Contact Form HTTP Trigger

See all 4 posts →
Software Development


BUILD A SUCCESSFUL DEV TEAM BY FOSTERING ENGAGEMENT

An "engaged employee" is defined as one who is fully absorbed by and
enthusiastic about their work and so takes positive action to further the
organization's reputation and interests.-Wikipedia page on Employee

 * Tom Faltesek

7 min read
Azure Functions


GHOST CONTACT FORM WITH AZURE FUNCTIONS

The primary downfall of Ghost's laser focused simplicity is that users have to
figure out how to fill the gap for any missing features they require. A common
example, a feature that I

 * Tom Faltesek

4 min read
Tom Faltesek
—
Azure Functions local.settings.json Secrets and Source Control
Share this

Tom Faltesek © 2023
Latest Posts Twitter LinkedIn


INTERESTED IN WORKING WITH TOM?

Send a brief description of your project and Tom will get back to you.

An error occured. Failed to send message.





Send



YOUR MESSAGE WAS SENT!

Tom will get back to you shortly.

Close