ConfigMaps & environment
Injecting configuration as env vars and files.
You build a container image once. Then you ask that same image to run on your laptop, in staging, and in production, and to behave differently in each place without a single rebuild. So what changes between those places? Configuration. Which database to point at. How loud the logs should be. Whether a half-finished feature is switched on. Bake those into the image and every tweak means a fresh build, a new tag, and a redeploy just to flip one log level. A ConfigMap is where that configuration lives instead, so none of it gets welded into the image.
Think of the image as a coffee machine that ships identical to every cafe. The ConfigMap is the little card each cafe slots in: these beans, this temperature, this strength. Same machine everywhere, different card, different cup. Change the card and the next cup changes; nothing about the machine itself moves. In Kubernetes terms a ConfigMap is a namespaced object (it lives inside one namespace, the way a file lives inside one folder) that holds configuration as plain key-value pairs. Non-sensitive values only. Passwords, tokens, and API keys belong in a Secret, which is the next lesson.
Two ways to make one
You rarely hand-write the YAML for these. Two flags on kubectl create cover most of what you need. Use --from-literal for a value you type inline, and --from-file to pull a whole file off disk and store it under a key. Whichever flag you reach for, you end up with the same kind of object; only the keys differ.
kubectl create configmap app-config \--from-literal=LOG_LEVEL=info \--from-file=app.yaml=./app.yaml
configmap/app-config created
Read it back and you can see exactly what got stored. The file landed as a block of text under its key. The literal is just a string.
kubectl get configmap app-config -o yaml
apiVersion: v1kind: ConfigMapmetadata:name: app-confignamespace: defaultdata:LOG_LEVEL: infoapp.yaml: |server:port: 8080
Getting it into a container
A ConfigMap on its own does nothing. It just sits there. To matter, it has to reach the process inside your Pod (the smallest thing Kubernetes runs: one or more containers that share a single network address). There are two doors into that process. The first is environment variables, the named settings a program reads the moment it launches. envFrom turns every key in the map into one variable; configMapKeyRef pulls a single key in under a variable name you choose, for when you only want one value. The second door is files: mount the map as a volume and each key becomes a file on a path you pick. A rule of thumb: reach for env vars when the app was written to read settings from its environment, and reach for a volume when it expects a real config file at a known path, like an nginx.conf or an application.yaml it opens from disk. Plenty of apps use both at once, which is exactly what the Deployment below does.
apiVersion: apps/v1kind: Deploymentmetadata:name: webspec:replicas: 1selector:matchLabels:app: webtemplate:metadata:labels:app: webspec:containers:- name: appimage: nginx:1.27envFrom:- configMapRef:name: app-configvolumeMounts:- name: cfgmountPath: /etc/appvolumes:- name: cfgconfigMap:name: app-config
Apply that, then look inside the running container to check both doors worked. One quirk shows up right away. envFrom quietly skips app.yaml, because a dot isn't a legal character in an environment variable's name, so that key shows up only as the mounted file. Kubernetes even records an event about the skipped key if you go looking for it.
kubectl exec deploy/web -- sh -c 'echo "LOG_LEVEL=$LOG_LEVEL"; cat /etc/app/app.yaml'
LOG_LEVEL=infoserver:port: 8080
What a change actually does
Here is where administrators get burned. How a ConfigMap edit reaches a running Pod depends entirely on which door you used, and the two doors behave in opposite ways. Environment variables are a snapshot. The operating system hands each program its set of variables exactly once, at the instant it starts, then never touches them again. Edit the ConfigMap and the running container's env vars don't budge. Files are the opposite. The kubelet (the Kubernetes agent running on every node, the actual machine that hosts your Pods) keeps the mounted volume in sync with the object. On its regular sync loop it writes the new content into a fresh hidden directory, then flips a symlink named ..data to point at that new directory in a single step. Think of a stagehand swapping the whole set behind a curtain: the container never catches a file half-written mid-change. Budget up to about a minute of lag. And the app only benefits if it actually re-reads the file on its own.
kubectl patch configmap app-config --type merge \-p '{"data":{"LOG_LEVEL":"debug","app.yaml":"server:\n port: 9090\n"}}'
configmap/app-config patched
Wait a minute, then run the same check again. The mounted file has changed. The env var has not.
kubectl exec deploy/web -- sh -c 'echo "LOG_LEVEL=$LOG_LEVEL"; cat /etc/app/app.yaml'
LOG_LEVEL=infoserver:port: 9090
To move the env-injected value, you have to replace the Pod. kubectl rollout restart does it cleanly: it brings up new Pods that read the fresh snapshot at startup, then drains the old ones away. It feels like a workaround the first time you see it, but it is the standard move, because env config is fixed in a live container, full stop. Teams automate this so nobody has to remember. Stamp a hash of the ConfigMap into the Pod template as an annotation, and any edit to the map changes the hash, which changes the template, which makes the Deployment roll all by itself.
kubectl rollout restart deploy/web
deployment.apps/web restarted
When it's missing, and when it must not move
If a container references a ConfigMap, or a single key inside one, that isn't there, the kubelet can't finish assembling the container's configuration and the Pod parks in CreateContainerConfigError. That one status covers both cases: a whole ConfigMap that doesn't exist, and a single missing key inside one that does. kubectl describe pod names the exact key it couldn't find, which is usually all you need. Either create the key, or set optional: true on the reference if the app can start without it. At the other extreme, once a ConfigMap has settled and you know it won't change, you can set immutable: true. That blocks every edit (to change it now you delete and recreate), and it tells the kubelet to stop watching the object. In a cluster running thousands of Pods, all those dropped watches lift real load off the API server, the control-plane process every kubectl command talks to.
Immutable ConfigMaps prevent silent drift. Use them when you want deploys, not live edits, to change behavior.
Huge ConfigMaps hurt the API and etcd. Keep blobs in object storage and put pointers in config.
Name keys carefully. Apps that require exact filenames want items with path mappings, not the whole map dumped blindly. Immutable ConfigMaps prevent silent drift.
Try this
Create a ConfigMap from literals and a file, mount it into a pod, and also inject one key as an env var. Change the ConfigMap and note what updates live versus what needs a restart.
$ kubectl create configmap app-config \--from-literal=LOG_LEVEL=info \--from-file=app.yaml=./app.yaml$ kubectl get configmap app-config -o yaml$ kubectl exec deploy/web -- sh -c 'echo "LOG_LEVEL=$LOG_LEVEL"; cat /etc/app/app.yaml'$ kubectl patch configmap app-config --type merge \-p '{"data":{"LOG_LEVEL":"debug","app.yaml":"server:\n port: 9090\n"}}'$ kubectl exec deploy/web -- sh -c 'echo "LOG_LEVEL=$LOG_LEVEL"; cat /etc/app/app.yaml'$ kubectl rollout restart deploy/web
Takeaway
ConfigMaps carry non-secret config as env or files. File mounts can update; env vars generally need a new pod. Do not put credentials here.