Configuration files are a really handy way to store everything that changes between environments, or anything that we’d like to be able to adjust at runtime. Usually, we store things like database connection strings, or timeout values there. In dotnet, the most straightforward way to achieve this is to read from appsettings.json
or Web.config
in case of .NET Framework applications.
However, dealing with configuration files in the containers can give you a little bit of a headache at first. Since they have their filesystem separated, how are you supposed to throw a file in there, at runtime ?
Configs inside docker
Copying configuration files into the container image seems like the most obvious way to address the issue. Just place COPY appsettings.json .
into the Dockerfile and you are ready to go. But .. are you ?
The problem is, that this way you bind your application with it’s configuration files forever. Once you want to adjust the values, you need to rebuild the whole image and redeploy application. Imagine that - each time you want to change timeout, you need to go through the entire pipeline.
Thankfully, there are far better ways of configuring your application available.
Environmental variables
Environmental variables have their origin back in UNIX. They are the standard way of supplying runtime configuration to the process, almost natural one for anyone using linux. Probably you already know some of them: PATH
, HOME
etc..
Kubernetes and docker are no different there, as configuring processes using envs is built into both tools since.. always. It’s handy, as when you move your config out of the container itself, your container is far more portable. It’s not limited to a single config file, but can run with literally any configuration instead.
Besides, it’s also the easiest way of passing configuration in Kubernetes:
apiVersion: v1
kind: Pod
metadata:
name: my-awesome-pod
spec:
containers:
- name: hello-container
image: hello_asp_net_core:latest
env:
- name: MyOptions__MyValue
value: "Hello from the environment"
- name: AllowedHosts
value: "contoso.com"
That’s it, really. However, there is also another, a bit more recent and Kubernetes-native way..
Configmaps
To handle runtime configuration nicely, Kubernetes has a dedicated resource called ConfigMap
. How are they different from environmental variables, you might ask ?
- They are a global cluster resource, meaning many containers can reference the same configs. Quite handy, for e.g. URLs of external services you need to integrate wit
- They can also store files and mount them somewhere in the container.
Having example ConfigMap manifest:
apiVersion: v1
kind: ConfigMap
metadata:
name: my-config
data:
MyOptions__MyValue: "I’m being read from configmap"
AllowedHosts: "contoso.com"
We can then “mount” it to the container as environmental variables:
apiVersion: v1
kind: Pod
metadata:
name: my-awesome-pod
spec:
containers:
- name: hello-container
image: hello_asp_net_core:latest
envFrom:
- configMapRef:
name: my-config
This will expose all the key value pairs from the configmap as environmental variables set in the container.
Moreover, as already mentioned, configmaps can store files as well:
apiVersion: v1
kind: ConfigMap
metadata:
name: appsettings_configmap
data:
appsettings.json: |
{
"MyOptions": {
"MyValue": "file inside configmap"
}
}
Those files can then be then mounted within container filesystem like so:
apiVersion: v1
kind: Pod
metadata:
name: my-awesome-pod
spec:
containers:
- name: hello-container
image: hello_asp_net_core:latest
volumeMounts:
- name: config-volume
# where to place it in the container
mountPath: /app/appsettings.json
# what files do you want
subPath: appsettings.json
volumes:
- name: config-volume
configMap:
name: appsettings_configmap
If you have something confidential, like private keys, that you need to pass to your container you might also take a look at Secret
s. At the time of writing, they are almost the same as ConfigMaps, but it gives Kubernetes more context (semantics) on what’s inside. Besides, it’s more future proof as in the future distinction between the two will be much more significant.
How do I read it then ?
Environmental variables were not very well supported in .NET Framework. Actually, all you could do in barebone framework was Environment.GetEnvironmentVariable
. Dotnet core brought a breakthrough there: configuration providers. It’s an awesome way to read configuration values from a variety of sources like:
- JSON/XML files
- Command-line arguments
- Custom providers (installed or created)
- Directory files
- Environment variables
- In-memory .NET objects
- Azure Key Vault
- Azure App Configuration
Moreover, this configuration is cascading/hierarchical. Meaning, you can, for example, override setting in the JSON file with an environmental variable. This way, you can keep the file for local development, but override respecting values using environmental variables in Kubernetes.
How do you do that in code? Well, turns out it’s actually really simple:
namespace HelloAspNetCore
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables()
.Build();
return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}
}
You need to use ConfigurationBuilder
to build up the config first, while specifying the order of overrides. Then you just need to call UseConfiguration
so that the configuration provider gets registered.
Having done so, you can use IConfiguration
interface later on, to register strongly typed configuration POCO classes in the IOC container, like so:
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.Configure<MyOptions>(Configuration.GetSection(nameof(MyOptions)));
}
This simple setup will make MyOptions
available within IOC container under IOptions<MyOptions>
interface. Meaning, you can just take IOptions<MyOptions>
as a constructor parameter and the framework will supply it.
Summing things up, let’s say this is your appsettings.json
:
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"MyOptions": {
"MyValue": "I'm being read from appsetings.json"
}
}
If you now set MyOptions__MyValue
or MyOptions:MyValue
environmental variable, it’ll override setting from the JSON file. Quite handy, isn’t it ?
Summary
As you can see, there are many ways to deal with runtime configuration in Kubernetes and even more ways to to read it in dotnet core. You can use files, environmental variables , some cloud native solutions, redis.. How do I choose the correct one ?
The end game is, it doesn’t really matter which way you go, as long as you separate your runtime config from the container itself. This way you make your container portable, easy to operate and save yourself from troubles.
Comments