Traditionally, containerised applications have gotten their configuration from environment variables or configuration files mounted into the container. But since containers (including their environment variables and mounts) are effectively immutable, changing the configuration requires that the container is re-created.

For static configuration, this is fine, but for dynamic configuration you’re better off using something like Consul K/V or a database for your configuration data.

And if you’re hosting your application in Kubernetes, you now have an additional option :-)

Kubernetes configuration resources

Kubernetes provides 2 primary mechanisms for configuring applications:

ConfigMap

A ConfigMap is a key / value store for application settings. It can contain any data you like (but is more suited to textual data).

Secret

A Secret is similar to a ConfigMap, but (among other things) its data is Base64-encoded; if you want to store a certificate or other non-textual key material then Secrets have got you covered.

Supplying app configuration

The key / value pairs that comprise ConfigMaps and Secrets are most commonly used to populate either environment variables or the content of files mounted into containers.

For example, here’s a ConfigMap and a partial Pod specification that propagates its keys / values to the pod’s container as environment variables.

---
kind: ConfigMap
apiVersion: v1
metadata:
  name: demo-config
  namespace: default
data:
  key1: 'Hello, World'
  key2: 'Goodbye, Moon'
---
kind: Pod
apiVersion: v1
metadata:
  name: demo-pod
  namespace: default
spec:
  containers:
    - name: container1
      env:
        - name: GREETING # = 'Hello, World'
          valueFrom:
            configMapKeyRef:
              name: demo-config
              key: key1
        - name: FAREWELL # = 'Goodbye, Moon'
          valueFrom:
            configMapKeyRef:
              name: demo-config
              key: key2

And here’s an example of mounting a Secret’s keys / values as files in a container:

---
apiVersion: v1
kind: Secret
metadata:
  name: demo-secret
type: Opaque
data:
  certificate.pem: YWRtaW4=     # Mounted below at /etc/ssl/certificate.pem
  private.key: MWYyZDFlMmU2N2Rm # Mounted below at /etc/ssl/private.key
---
kind: Pod
apiVersion: v1
metadata:
  name: demo-pod
  namespace: default
spec:
  containers:
    - name: container1
      volumeMounts:
        - name: ssl
          mountPath: /etc/ssl # Keys used as file names, values used as file contents
  volumes:
    - name: ssl
      secret:
        secretName: demo-config

Microsoft.Extensions.Configuration

.NET Core (ASP.NET Core, more specifically) provides an abstraction for application configuration in the form of Microsoft.Extensions.Configuration (and Microsoft.Extensions.Options on top of that).

Out of the box, you already have providers that support sourcing configuration from JSON files, environment variables, command-line arguments, etc. For some configuration sources, it will even automatically reload the configuration if the source data changes.

KubeClient.Extensions.Configuration

The KubeClient library supports configuration providers that use a Kubernetes ConfigMap or Secret as the source for configuration data. Optionally, if the ConfigMap or Secret is modified after the application is started then the application configuration will be automatically updated.

Using a ConfigMap for configuration

KubeClientOptions kubeClientOptions = Config.Load().ToKubeClientOptions();

IConfiguration configuration = new ConfigurationBuilder()
    .AddKubeConfigMap(kubeClientOptions,
        configMapName: "config-from-configmap",
        kubeNamespace: "default"
    )
    .Build();

string greeting = configuration["key1"];
Console.WriteLine("Greeting: {0}", greeting);

string farewell = configuration["key2"];
Console.WriteLine("Farewell: {0}", farewell);

Automatically reloading

You can also enable automatic reloading of the configuration when the underlying ConfigMap changes:

KubeClientOptions kubeClientOptions = Config.Load().ToKubeClientOptions();

IConfiguration configuration = new ConfigurationBuilder()
    .AddKubeConfigMap(kubeClientOptions,
        configMapName: "config-from-configmap",
        kubeNamespace: "default",
        reloadOnChange: true
    )
    .Build();

// We want to be notified each time the configuration changes.
var reloadToken = configuration.GetReloadToken();
reloadToken.RegisterChangeCallback(OnConfigChanged, state: null);

void OnConfigChanged(object state)
{
    string greeting = configuration["key1"];
    Console.WriteLine("Updated greeting: {0}", greeting);

    string farewell = configuration["key2"];
    Console.WriteLine("Updated farewell: {0}", farewell);

    // Reload tokens only work once, then you need a new one.
    reloadToken = configuration.GetReloadToken();
    reloadToken.RegisterChangeCallback(OnConfigChanged, state: null);
}

IOptions<T>

Once you have your configuration (e.g. in an ASP.NET Core Startup class), you can configure the dependency-injection system to make typed options available to represent it.

So if you define an options class:

public class MyAppOptions
{
    public string Greeting { get; set; }
    public string Farewell { get; set; }
}

Then configure your configuration providers:

WebHost.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration(
        configuration => configuration.AddKubeConfigMap(
            clientOptions: KubeClientOptions.FromPodServiceAccount(),
            configMapName: "demo-config",
            kubeNamespace: "default",
            reloadOnChange: true
        )
    )
    .UseStartup<Startup>()

Then configure your service container:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    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.AddOptions();
        services.Configure<MyAppOptions>(Configuration);
    }
}

Finally, inject the options into a controller:

[Route("api/v1/greetings")]
public class GreetingController
{
    public GreetingController(IOptions<MyAppOptions> options)
    {
        Options = options.Value;
    }

    MyAppOptions Options { get; }

    [HttpGet("hello-goodbye")]
    public IActionResult HelloGoodbye()
    {
        return Ok(new
        {
            Greeting = Options.Greeting,
            Farewell = Options.Farewell
        });
    }
}

If you call this web API before and after changing the ConfigMap, you will see that the application options change automatically.