Why this matters
What you need to know already: environment variables (6.1, 2.21), Docker volumes and bind mounts (11.22), symlinks (4.13), pods and Deployments (15.14, 15.16), rollouts (15.16).
The same image runs in dev and prod; only its settings differ - log level, greeting text, a config file. Baking settings into the image means rebuilding it for every change. Kubernetes keeps them in a separate object, a ConfigMap (a named set of key/value pairs, each value a string or a whole file), and hands it to the container. The surprise this lesson is about: how you hand it over decides whether a change ever reaches the running app.
Creating one
k create configmap NAME (short: cm) builds one from the command line: --from-literal=KEY=VALUE adds one key, --from-file=FILE adds a file's content under its file name (or KEY=FILE to rename it), --from-env-file=FILE reads KEY=VALUE lines.
k create configmap app-config --from-literal=LOG_LEVEL=info --from-literal=GREETING=Hello
k create configmap nginx-conf --from-file=default.conf # key = file name
k create configmap nginx-conf --from-file=site.conf=default.conf # key = site.conf
k create configmap env-config --from-env-file=app.env # KEY=VALUE lines
$ k get cm app-config -o yaml
apiVersion: v1
data:
GREETING: Hello
LOG_LEVEL: info
kind: ConfigMap
metadata:
name: app-config
namespace: default
...
The object has no spec: just data, a map of key to string. Values are always strings. In YAML that means quoting anything that looks like something else: PORT: "8080", DEBUG: "true" - an unquoted 8080 is a number and the apiserver rejects the ConfigMap (cannot unmarshal number into Go struct field ConfigMap.data of type string). Max 1 MiB per ConfigMap.
Consuming it, four ways
In the pod template: env + valueFrom.configMapKeyRef copies one key into an environment variable; envFrom copies all keys; a volume of type configMap turns keys into files, mounted with volumeMounts at a path in the container (like docker run -v, 11.22). subPath mounts a single file instead of a directory.
spec:
containers:
- name: app
image: registry.lab/shop/greeter:1.0
env:
- name: GREETING # 1. one key as one variable
valueFrom:
configMapKeyRef:
name: app-config
key: GREETING
envFrom:
- configMapRef: # 2. every key as a variable
name: app-config
prefix: APP_ # (optional: APP_GREETING, APP_LOG_LEVEL)
volumeMounts:
- name: cfg
mountPath: /etc/greeter # 3. every key as a file in a directory
- name: cfg
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf # 4. one key as one file, via subPath
volumes:
- name: cfg
configMap:
name: app-config
items: # optional: only these keys, renamed
- key: GREETING
path: greeting.txt
The critical difference
Environment variables are resolved once, when the container starts. They are copied into the process's environment by the kubelet - like Environment= in a systemd unit (2.21), read once at start. Change the ConfigMap and a running container never sees it - not in a minute, not in a day - until the container is restarted.
Mounted files are updated in place. The kubelet periodically re-syncs ConfigMap volumes (its sync loop plus the object cache - in practice up to about a minute) and swaps the files atomically. The app sees the new content the next time it reads the file.
You can watch both with the lab's greeter app, which reads GREETING from its environment once at startup and re-reads /etc/greeter/message.txt every 10 seconds:
(k logs deploy/greeter --tail=2 prints the last 2 log lines of one of the Deployment's pods; k patch cm NAME -p 'JSON' changes fields in place; k exec deploy/greeter -- CMD runs a command in one of its pods.)
# greeter is what the next mission builds
k logs deploy/greeter --tail=2
2026-09-23T10:00:16Z greeting(env)="Hello" message(file)="first message"
k patch cm greeter-config -p '{"data":{"GREETING":"Salut","message.txt":"second message"}}'
configmap/greeter-config patched
k logs deploy/greeter --tail=2 # about a minute later
2026-09-23T10:01:16Z greeting(env)="Hello" message(file)="second message"
The file changed; the env var did not. Inside the container:
k exec deploy/greeter -- printenv GREETING
Hello
k exec deploy/greeter -- cat /etc/greeter/message.txt
second message
How the atomic swap works - look at the directory:
# greeter is what the next mission builds
k exec deploy/greeter -- ls -la /etc/greeter
total 12
drwxr-xr-x 2 root root 4096 Sep 23 10:01 .
drwxr-xr-x 2 root root 4096 Sep 23 10:01 ..
drwxr-xr-x 2 root root 4096 Sep 23 10:01 ..2026_09_23_10_01_12.201312345
lrwxrwxrwx 1 root root 31 Sep 23 10:01 ..data -> ..2026_09_23_10_01_12.201312345
lrwxrwxrwx 1 root root 18 Sep 23 10:01 message.txt -> ..data/message.txt
message.txt is a symlink to ..data/message.txt; ..data is a symlink to a timestamped directory. An update writes a new timestamped directory and flips ..data in one rename - the app never reads a half-written file. It also means inotify watches on the file itself break on update (inotify is the Linux "tell me when this file changes" facility); watch the directory.
Two consequences people learn in production:
- subPath mounts never update. A subPath mount binds one file directly, not through the
..datasymlink, so it is frozen at container start like an env var. Mountingnginx.confwith subPath and expecting hot reload does not work. - Hot reload needs the app to cooperate. The file changes; whether the process re-reads it is up to the app (nginx needs
nginx -s reload, many frameworks need an explicit refresh mechanism). Many apps read config once at startup, making the volume no better than env.
The rollout problem
Changing a ConfigMap never restarts pods. Nothing in Kubernetes links "this ConfigMap changed" to "roll this Deployment". Three ways to make a change take effect:
1. Restart the rollout by hand
k rollout restart deploy/greeter
Adds kubectl.kubernetes.io/restartedAt to the pod template - a template change - so the Deployment rolls every pod, respecting maxUnavailable.
2. A checksum annotation in the template (the pattern deploy tools automate)
spec:
template:
metadata:
annotations:
checksum/config: 3f7a9c... # sha256 of the ConfigMap's content
sha256sum greeter-config.yaml # recompute on every change
The config change changes the checksum, which changes the template, which rolls the Deployment - automatically, through the normal rollout machinery.
Later (Ch 25): Helm, a templating tool for manifests, computes this checksum for you on every deploy.
3. Versioned names
greeter-config-v7 -> greeter-config-v8, and the Deployment references the new name. Also a template change, and the old ConfigMap stays around for rollback. Some manifest tools generate such names for you, with a hash of the content as the suffix.
immutable
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-v8
immutable: true
data: {...}
Immutable ConfigMaps cannot be edited (data: Forbidden: field is immutable when immutable is set) - only deleted and recreated - and the kubelet stops watching them (a watch is a long-lived request asking the API server to push every change). On a cluster with thousands of pods that removes a large share of the API server's load. It pairs naturally with versioned names.
Missing ConfigMaps
A pod that references a ConfigMap that does not exist does not start:
as a volume: Warning FailedMount MountVolume.SetUp failed for volume "cfg" : configmap "app-config" not found
-> STATUS ContainerCreating, forever
as env: Warning Failed Error: configmap "app-config" not found
-> STATUS CreateContainerConfigError
missing key: Error: couldn't find key DB_URL in ConfigMap default/app-config
optional: true on the reference makes it start anyway with the variable unset or the directory empty.
What you can now do:
- Create a ConfigMap and feed it to a pod as env vars, files, or one file.
- Predict which changes a running pod sees (files, after up to a minute) and which it never sees (env, subPath).
- Roll a Deployment after a config change: rollout restart, checksum annotation, versioned name.