Immutable configuration and config templates
A ConfigMap you can edit at any time is handy, and dangerous. One quick edit changes production with no review, no history, and no easy way back. This chapter shows two habits that make config as safe as code: never edit config, release a new version of it, and, for big config files, keep one template and fill in only the parts that differ.
The problem with editing live config
Imagine changing the recipe card in a busy kitchen while cooks are using it. Some dishes follow the old card, some the new one, and nobody remembers what the old card said.
$ kubectl edit configmap app-config # LOG_LEVEL: info → debug, MAX_UPLOAD_MB: 10 → 1Someone edits the ConfigMap directly. Typo included: 1 instead of 100.
posts-4kq2p started yesterday LOG_LEVEL=info MAX_UPLOAD_MB=10 posts-x7m2d restarted today LOG_LEVEL=debug MAX_UPLOAD_MB=1Pods that were already running keep the old env values; any Pod that restarts gets the new ones (chapter 18). Now replicas behave differently, and only some uploads fail.
$ kubectl rollout undo deployment/posts-depl deployment.apps/posts-depl rolled back # …still brokenRolling back the Deployment does not help: it points at the same ConfigMap, which still holds the bad value. The old value is gone.
immutable: true
One line locks a ConfigMap or Secret. After it is created, its data can never be changed:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-v1
immutable: true
data:
LOG_LEVEL: info
MAX_UPLOAD_MB: "10"
$ kubectl edit configmap app-config-v1
error: configmaps "app-config-v1" is invalid: data: Forbidden: field is immutable when `immutable` is set
- Safer: no accidental live edits.
- Lighter: the kubelet stops watching it for changes, which helps clusters with thousands of ConfigMaps.
- One way only: you cannot switch
immutableback off. To change values, create a new ConfigMap (or delete and recreate this one).
Versioned names
Give each version its own name, and point the Deployment at the version it should use:
$ kubectl apply -f app-config-v2.yaml # new name, new values, immutableCreate the new version next to the old one. Nothing running changes yet.
envFrom: - configMapRef: name: app-config-v2 # was app-config-v1 $ kubectl apply -f posts-depl.yamlChange the name in the Deployment. That is a change to the Pod template, so Kubernetes does a normal rolling update (chapter 10). Readiness probes protect you as usual.
$ kubectl rollout undo deployment/posts-deplBad values? Roll back: the previous Pod template points at app-config-v1, which still exists, unchanged. A real, working rollback.
$ kubectl delete configmap app-config-v0Clean up very old versions once you are sure no Deployment revision needs them.
Let a tool name the versions
Renaming by hand is easy to forget. Kustomize, built into kubectl, generates the ConfigMap and adds a short hash of its content to the name. Change any value and the name changes, so the Deployment rolls automatically:
# kustomization.yaml
resources:
- posts-depl.yaml
configMapGenerator:
- name: app-config
literals:
- LOG_LEVEL=info
- MAX_UPLOAD_MB=10$ kubectl apply -k .
configmap/app-config-5g2h7mt9kc created
deployment.apps/posts-depl configuredKustomize also rewrites app-config in the Deployment to app-config-5g2h7mt9kc. You write the plain name; the tool handles versions.
Config inside an image
Another way to make config versioned and unchangeable: package the config files into their own small container image, such as posts-config:prod-2026-10. An init container copies them into a shared emptyDir (chapter 30) before the app starts.
initContainers:
- name: config
image: registry.example.com/posts-config:prod-2026-10
command: ["cp", "-r", "/config/.", "/etc/app/"]
volumeMounts: [{ name: app-config, mountPath: /etc/app }]
containers:
- name: posts
image: micro-posts:1.0
volumeMounts: [{ name: app-config, mountPath: /etc/app }]
volumes:
- name: app-config
emptyDir: {}
When it is worth it: config bigger than a ConfigMap's 1 MiB limit, or when you want config to go through the same registry, scanning and promotion steps as your images. Otherwise immutable ConfigMaps are simpler.
Configuration templates
Sometimes a config file is long, but only a few lines differ between dev and production. Copying the whole file per environment means three files that slowly drift apart. Instead, keep one template with placeholders, and fill them in when the Pod starts:
# ConfigMap posts-proxy-template, key default.conf.template
server {
listen 8080;
client_max_body_size ${MAX_UPLOAD_MB}m;
location / {
proxy_pass http://${UPSTREAM_HOST}:4000;
}
}One file for every environment. Only ${MAX_UPLOAD_MB} and ${UPSTREAM_HOST} differ.
containers:
- name: proxy
image: nginx:1.27
env:
- name: MAX_UPLOAD_MB
value: "10"
- name: UPSTREAM_HOST
value: posts-srv
volumeMounts:
- name: templates
mountPath: /etc/nginx/templates # nginx fills these in at startup
volumes:
- name: templates
configMap:
name: posts-proxy-templateThe official nginx image already does this: every *.template file in /etc/nginx/templates is filled in with env vars (using envsubst) when the container starts. For apps without such a feature, an init container runs a template tool (envsubst, gomplate) and writes the finished file into an emptyDir the app reads.
$ kubectl exec deployment/posts-proxy -- cat /etc/nginx/conf.d/default.conf
server {
listen 8080;
client_max_body_size 10m;
location / {
proxy_pass http://posts-srv:4000;
}
}The finished file exists only inside the running container. Production gets the same template with different env values.
Which approach?
| Situation | Use |
|---|---|
| A few settings, small team, learning | Plain ConfigMap; restart after changes (chapter 18) |
| Production, need history and rollback | Immutable, versioned ConfigMaps, ideally generated by Kustomize or Helm |
| Big config file, only a few values differ per environment | Template + env vars |
| Config over 1 MiB, or must follow image promotion | Config image + init container |
- You need to change
LOG_LEVELin an immutable ConfigMapEdits are rejected. Create app-config-v3 and point the Deployment at it.
- After the change you want the Pods to pick it up automatically
A new name changes the Pod template, which triggers a rolling update by itself.
Recap
- Editing live config causes drift between Pods and makes rollback impossible.
immutable: truelocks a ConfigMap or Secret for good and lightens the kubelet's work.- Versioned names (
-v2, or a Kustomize hash) turn a config change into a normal rolling update with real rollback. - Config images carry big or tightly controlled config, copied in by an init container.
- Templates keep one file and fill in per-environment values at startup.
Quick check
kubectl rollout undo fix bad config with versioned ConfigMaps, but not with one edited ConfigMap?Show answer
Rollback restores the previous Pod template. With versioned names, that template points at the old ConfigMap, which still has the old values. With one edited ConfigMap, the old template points at the same, already changed object.
Immutable configuration: common questions
What does immutable: true do on a ConfigMap?
It forbids any change to its data after creation and lets the kubelet stop watching it. To change values you create a new ConfigMap.