App Settings: Why We Use Them
Application settings are a vital part of any app as they provide the ability to alter how your app operates without needing to update and redeploy the code.
This means you can use the same version (and in particular, build) of the code across your different environments, e.g. development, staging, and production.
A perfect example of where app settings are important is when your app needs to connect to a database. You shouldn't be connecting to the same database in both your development and production environments for lots of reasons, and so being able to swap this out at runtime and without needing to change your code is incredibly useful.
Why we use app settings
Imagine a scenario where we didn't have app settings. As we moved our app from the development to the staging environment, we'd have to go into the code, update the connection string to point to the new database, build the code, and deploy the code to staging. That's also not including any pull request and code review processes (which you should have!).
That might not sound too bad, but we'd have to then repeat that process to deploy our changes to production...and then every time we made any change...no matter how small...
Over time, this just doesn't scale, and it gets even more complicated when there are multiple developers working on the project. The chances of making a mistake are very high, and the consequences of showing the wrong database to your end users are not worth the risk!
Different builds between environments is a problem
Consider this as well. There will come a time when we've deployed to the staging environment and our app has been tested and is ready to push to production. We would then go through the steps outlined above, one of which would be to rebuild the app. But how do we know this version would work the same way as the one that was tested? The answer is, we don't.
It's now a completely different build that is being deployed, which could have a number of differences from the previous version. Perhaps this new build has used a slightly different version of a framework or tool which has altered some behaviour. Or an environment variable was different on the build server which has caused a change.
These minor differences could result in issues that are incredibly difficult to debug. Some of the risks can be reduced by using a consistent CI/CD process (which again, you should be!), but they don't get rid of it entirely.
It's considered an anti-pattern to use different builds across environments. Having app settings that are read at runtime mean we can use the same build across each.
How to use app settings
App settings can be added to apps in many different ways, and will depend on the framework you are using.
Environment Variables
A common approach is for your application to read environment variables from the system it is running on. These can then be changed outside of your application without having to update the code.
Many frameworks will integrate with environment variables by default, for example the default host in .NET will read environment variables in and allow you to use them in your app.
Other frameworks will provide support for .env
files, which are a common way to define environment variables to be populated as part of your app.
MY_ENVIRONMENT_VARIABLE=FOO
MY_OTHER_ENVIRONMENT_VARIABLE=BAR
Configuration Files
Another common approach is to use configuration files to define the app settings that are needed in your app. Once the code is built, you can then simply swap this file out for an alternative configuration file between environments.
A good option is to use JSON files for configuration, as this then allows you to group configuration into different sections.
For example, ASP.NET Core includes appsettings.json
in the default application template. You can then also have separate configuration files such as appsettings.Development.json
or appsettings.Production.json
to alter the configuration between environments.
{
"MyAppSetting": "My Value",
"ThisIsAGroupOfSettings": {
"Setting1": "foo",
"Setting2": "bar"
}
}
Handling sensitive values
Often your app settings will store sensitive values that may need to change outside of the code, for example, database connection strings, credentials for APIs etc.
It is a major security risk and bad practice to include any of these sensitive values in your application code as part of source control, and so you want to avoid checking in any files that contain them.
At the same time, having the configuration files in source control provides many benefits:
- Its easier for other developers to onboard and setup the solution
- It helps to understand what configuration is used in the app
So how do we ensure we keep sensitive values out of source control, but still get the benefits of having the configuration in there?
Option 1 - Replace the sensitive values manually
This would involve having the configuration files in source control (but not populating the sensitive values), then replacing them at the start of each development session.
Hopefully you can see the problems with this approach - it would be a pain to have to add the sensitive values in all the time, and it would be far too easy to forget to take them back out and accidentally commit the sensitive values.
So we'll give this one a miss...
Option 2 - Use a local configuration file
This would involve having another file that's used to populate the sensitive configuration values, which is only stored locally and not in source control. The original configuration file would still be in source control, but would not contain any sensitive values:
// appsettings.json - checked into source control
{
"AppSetting1": "foo",
"AppSetting2": "bar",
"Password": "<placeholder>"
}
// appsettings.local.json - NOT checked into source control
{
// AppSetting1 and AppSetting2 are optional
"AppSetting1": "foo",
"AppSetting2": "bar",
"Password": "MySuperSecretPassw0rd"
}
The trick is that the local configuration file should always overwrite the one in source control - that way when you are running locally it actually uses the value for the password, and not just the placeholder.
When you deploy your app, you will need a mechanism to replace the default file in source control as part of the build, or combine it with environment variables on the deployment server to overwrite the sensitive values in the configuration.
That might sound like a lot of setup, but it is absolutely worth it to help your overall development experience. This setup is easy to configure in .NET with the default host builders, and using User secrets
for the local configuration file.
This approach is the one I recommend and has served me very well on many projects.
How to ensure the local file doesn't get committed
The easiest and best way to ensure the local configuration file never ends up in source control is to include it in your .gitignore
file, for example:
# .gitignore
appsettings.local.json
You need to make sure that it hasn't already been committed or Git will still track it! If it has already been committed, then you will need to delete the file, commit the change, then add the .gitignore
record to tell Git to ignore the file.
What to do if sensitive values have been committed
Mistakes happen - you can sometimes accidentally commit a configuration file with sensitive values into source control. In this case, simply deleting the file from the repository won't remove all references to the sensitive data. It will still be contained in the commits and history of the repo.
The best way to solve this is to reset any passwords/credentials that were stored in the file so that none of the values work anymore - that way a potential attacker can't access even if they managed to get the values from the history.
You can also use tools such as bfg-repo-cleaner
to remove private data from your Git repository data.
The actual best approach though - avoid committing them in the first place!
Closing thoughts
App settings are a vital part of application development to ensure you have a robust, secure, and efficient development process. If you're building apps you should definitely be using them!
The implementation may differ between frameworks, but the benefits are the same. Choose the approach that works for you and your team - just don't forget to handle sensitive values properly!