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
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&k=6LdTsHIUAAAAAF_97o4PbFuzPwMJbbIq73IxWoNU&co=aHR0cHM6Ly93d3cudG9tZmFsdGVzZWsuY29tOjQ0Mw..&hl=de&v=0hCdE87LyjzAkFO5Ff-v7Hj1&size=invisible&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