ConfigMaps and Secrets
Reaching IConfiguration from ConfigMaps and Secrets, through environment variables or mounted files, and what a Secret is not.
builder.Configuration.AddKeyPerFile("/etc/secrets", optional: true, reloadOnChange: true);The same image has to run in test and in production with different settings, so the settings cannot live in the image. Kubernetes keeps them in two objects. A ConfigMap holds plain key and value pairs. A Secret holds the same thing for passwords, tokens and keys, with slightly different handling.
Two ways in
Either one can reach the container as environment variables or as files in a mounted directory. WebApplication.CreateBuilder already reads environment variables, and it adds them after appsettings.json, so they win. In a variable name, a double underscore is the section separator: Database__Host becomes the key Database:Host. A colon is not used, because it is not valid in a variable name on every platform.
For files, mount the object as a volume and add a provider: AddJsonFile for one mounted appsettings file, or AddKeyPerFile for a directory where each file name is a key and the file content is the value.
A Secret is not encrypted
A Secret's data values are base64, which is an encoding anyone can reverse. Kubernetes stores Secrets unencrypted in etcd unless the cluster has encryption at rest switched on, and anyone who can read the Secret can read the password. Write the plain text under stringData and let the API server encode it.
What trips people up
Mounted keys are updated in a running pod eventually, but a file mounted with subPath never is. Environment variables never change for a running process.
Example
// In a console app this needs two packages:
// dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables
// dotnet add package Microsoft.Extensions.Configuration.Binder
using Microsoft.Extensions.Configuration;
// Kubernetes sets these from a ConfigMap. They are set here so the
// example stands on its own.
Environment.SetEnvironmentVariable("Database__Host", "orders-db");
Environment.SetEnvironmentVariable("Database__Port", "5432");
IConfiguration config = new ConfigurationBuilder()
.AddEnvironmentVariables()
.Build();
// The double underscore in the variable name became a colon in the key.
Console.WriteLine($"indexer: {config["Database:Host"]}");
// The same lookup written out in full. The indexer is shorthand for this.
Console.WriteLine($"section: {config.GetSection("Database").GetSection("Host").Value}");
// Binding the whole section onto a class, which is what real code does.
DatabaseOptions options = config.GetSection("Database").Get<DatabaseOptions>()!;
Console.WriteLine($"port: {options.Port}");
public sealed class DatabaseOptions
{
public string Host { get; set; } = "";
public int Port { get; set; }
}
// Output:
// indexer: orders-db
// section: orders-db
// port: 5432When to use it
- Point the same image at the test database in one namespace and the production one in another.
- Hand a database password to a pod without putting it in the image or in Git.
- Change a log level in production without rebuilding anything.
More examples
A ConfigMap as environment variables
env and envFrom do the same job. Name keys one by one while there are few of them and you want the pod spec to show what the app reads; switch to envFrom once the list is long. Both produce ordinary environment variables, so both need a new pod to pick up a change.
apiVersion: v1
kind: ConfigMap
metadata:
name: orders-config
data:
Database__Host: orders-db
Database__Port: "5432" # values are strings, so quote numbers
Logging__LogLevel__Default: Information
---
# inside the Deployment's container
env:
- name: Database__Host # longhand: one named key
valueFrom:
configMapKeyRef:
name: orders-config
key: Database__Host
envFrom:
- configMapRef: # shorthand: every key in the object
name: orders-configA Secret mounted as files
Files are the better choice for secrets: they do not show up in kubectl describe pod or in a crash dump of the environment block, and they can be updated without replacing the pod. The mount produces /etc/secrets/Database__Password containing the password and nothing else, no trailing newline.
apiVersion: v1
kind: Secret
metadata:
name: orders-secrets
type: Opaque
stringData: # plain text in, base64 in storage
Database__Password: pa55word-from-the-vault
---
# inside the Deployment's pod spec
volumes:
- name: secrets
secret:
secretName: orders-secrets
containers:
- name: api
volumeMounts:
- name: secrets
mountPath: /etc/secrets # one file per key
readOnly: trueReading the mounted directory
optional: true keeps the app startable on a developer machine where /etc/secrets does not exist. If a changed file is not noticed inside a container, set the environment variable DOTNET_USE_POLLING_FILE_WATCHER to 1, because change notifications are not delivered on every kind of mount.
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
// Each file in the directory becomes one key, and the provider's section
// delimiter is the double underscore: the file Database__Password
// becomes the configuration key Database:Password.
builder.Configuration.AddKeyPerFile("/etc/secrets", optional: true, reloadOnChange: true);
builder.Services.Configure<DatabaseOptions>(
builder.Configuration.GetSection("Database"));
var app = builder.Build();
// IOptionsSnapshot re-reads per request, so a reloaded file is visible.
app.MapGet("/config", (IOptionsSnapshot<DatabaseOptions> options) =>
new { host = options.Value.Host });
app.Run();
public sealed class DatabaseOptions
{
public string Host { get; set; } = "";
public string Password { get; set; } = "";
}
Discussion