ConfigMaps: settings outside the image
One image, many environments.
A chain coffee shop buys the same espresso machine for every branch. Built in one factory, shipped everywhere. The branch in Delhi runs it hotter than the one in London, so each shop keeps a small settings card taped to the side. Nobody rebuilds the machine to change the water temperature. They swap the card.
Your app works the same way. A container image is a frozen, ready-to-run copy of your app and everything it needs to start up. You build it once and ship that exact copy everywhere it runs. An environment just means one of those places: your own laptop, a shared test server, the real site your users visit. The settings that differ between environments, like which database to connect to or how chatty the logs should be, shouldn't be baked inside the image. Bake them in and you'd have to rebuild and reship the whole thing for every little change, even flipping a log level. Those settings belong on the card. In Kubernetes, that card is a ConfigMap: an object that stores plain configuration as key-value pairs (a name and its value, such as LOG_LEVEL set to info) and hands it to your running app.
Kubernetes runs your app inside a Pod. A Pod is the smallest unit Kubernetes runs, really just a thin wrapper around one or more running containers. You'll talk to your cluster (the whole group of machines Kubernetes manages) using kubectl, the command-line tool for Kubernetes. Let's make a ConfigMap and hand it to a Pod.
Make one and look at it
apiVersion: v1kind: ConfigMapmetadata:name: app-configdata:LOG_LEVEL: "info"app.conf: |server_port = 8080timeout = 30
That file is written in YAML, a plain-text format that describes configuration as indented key-value pairs. Two entries live under data. LOG_LEVEL is a single value your app can read. app.conf is a whole file: the vertical bar tells YAML to treat everything indented beneath it as one block of text, so you can paste a real config file straight in. Save it, then apply it.
kubectl apply -f app-config.yaml
configmap/app-config created
kubectl get configmap app-config
NAME DATA AGEapp-config 2 9s
The DATA column shows 2, one for each key you put in. The ConfigMap now lives in the cluster on its own, separate from any app that might use it. It's just sitting there, ready for a Pod to ask for it.
Two ways into the app
There are two ways a ConfigMap reaches your container, and the difference matters in a minute. The first is environment variables: values the operating system hands your program the moment it starts, like a note pinned up before the shift begins. The second is files: each key becomes a file on disk that your app can open whenever it wants. Some apps expect their settings as environment variables. Others want a config file at a path like /etc/app/app.conf. A ConfigMap can do both at once.
apiVersion: apps/v1kind: Deploymentmetadata:name: webspec:replicas: 1selector:matchLabels:app: webtemplate:metadata:labels:app: webspec:containers:- name: webimage: nginx:1.27envFrom:- configMapRef:name: app-configvolumeMounts:- name: cfgmountPath: /etc/appvolumes:- name: cfgconfigMap:name: app-config
A Deployment is the object that keeps a set of identical Pods running and restarts them for you when needed; replicas: 1 just means run a single copy. Two things wire the ConfigMap into the container. envFrom reads the ConfigMap's keys and turns them into environment variables. It only accepts valid variable names, so LOG_LEVEL comes through and app.conf is skipped, since a dot isn't allowed in a variable name. That's fine here, because we want app.conf as a file instead. That's the volume's job. Mounting means making those keys show up as files inside the container, here under /etc/app. Apply it, then check that both landed.
kubectl apply -f web-deploy.yaml
deployment.apps/web created
kubectl exec deploy/web -- printenv LOG_LEVELkubectl exec deploy/web -- cat /etc/app/app.conf
infoserver_port = 8080timeout = 30
printenv read the environment variable; cat read the file. Both values came out of the same ConfigMap, and neither one is baked into the nginx image. That's the whole point: one image, different settings, decided when it runs. Point a different ConfigMap at the same Deployment and the app behaves differently, no rebuild needed.
When the ConfigMap isn't there
The ConfigMap lives on its own, which means Kubernetes won't invent values it can't find. Point a container at a ConfigMap that isn't there, whether from a typo in the name or from applying the Pod before you create the ConfigMap, and it can't finish starting. Worth causing on purpose once, so you recognise the shape of it later. Here's a throwaway Pod that asks for a ConfigMap nobody ever made.
apiVersion: v1kind: Podmetadata:name: brokenspec:containers:- name: appimage: nginx:1.27envFrom:- configMapRef:name: does-not-exist
kubectl apply -f broken.yaml
pod/broken created
Now list the Pod.
kubectl get pod broken
NAME READY STATUS RESTARTS AGEbroken 0/1 CreateContainerConfigError 0 15s
Not Running, but CreateContainerConfigError. That's the kubelet, the node agent you'll meet again in a moment, saying it tried to build the container's environment from a ConfigMap and found nothing to build from. get pod tells you it's stuck; it won't tell you why. For that, describe the Pod and read the events at the bottom.
kubectl describe pod broken
Events:Type Reason Age From Message---- ------ ---- ---- -------Normal Scheduled 20s default-scheduler Successfully assigned default/broken to node-1Normal Pulled 6s (x3 over 19s) kubelet Container image "nginx:1.27" already present on machineWarning Failed 6s (x3 over 19s) kubelet Error: configmap "does-not-exist" not found
The last line names it exactly: configmap "does-not-exist" not found. Create that ConfigMap and the kubelet retries on its own, no manual restart needed. Reading the last event after a describe is how you'll untangle most stuck Pods, not only this one. Clean up the throwaway with kubectl delete pod broken.
Changing a setting later
Here's the part that trips people up. When you edit a ConfigMap, the change doesn't reach a running Pod the same way for both methods. Which one you picked earlier decides how much work the update takes now. Values that went in as environment variables are read once, at startup, then frozen. The process already copied them into memory, so editing the ConfigMap does nothing to it. Values mounted as files do refresh on their own.
That refresh is handled by the kubelet, the Kubernetes agent running on every node (a node is simply a worker machine in your cluster). It updates mounted ConfigMap files roughly once a minute. Even then, your app only notices if it re-reads the file, and plenty of apps read their config just once at boot.
So when you change config and want it to take effect, you normally restart the app's Pods. With a Deployment that's one safe command, and Kubernetes brings up fresh Pods, replacing the old ones one at a time so traffic keeps flowing.
kubectl rollout restart deployment/web
deployment.apps/web restarted
ConfigMaps hold non-secret config as keys you inject as env vars or files. Prefer config outside the image so one image promotes across environments. That is the whole point versus rebuilding for every LOG_LEVEL change.
Updates to a ConfigMap do not always restart pods. Env-from values are typically fixed at container start; file mounts can update depending on the kubelet sync. Prefer an explicit rollout restart when config must apply now.
Imagine shipping the wrong database hostname via ConfigMap to production: the image is fine, the cluster is fine, and the app is pointed at staging. Treat ConfigMap changes as production changes — review them like code.
Try this
Create a ConfigMap, run a pod that prints an env var from it, then change the map and restart to pick up the new value.
$ kubectl create configmap app-config --from-literal=LOG_LEVEL=infoconfigmap/app-config created$ kubectl run cfg --image=busybox:1.36 --restart=Never --env=LOG_LEVEL=unused --command -- sleep 300pod/cfg created$ kubectl set env pod/cfg --from=configmap/app-configpod/cfg env updated# recreate to see env-from cleanly:$ kubectl delete pod cfg$ kubectl run cfg --image=busybox:1.36 --restart=Never --overrides='{"spec":{"containers":[{"name":"cfg","image":"busybox:1.36","command":["sleep","300"],"envFrom":[{"configMapRef":{"name":"app-config"}}]}]}}'pod/cfg created$ kubectl exec cfg -- printenv LOG_LEVELinfo$ kubectl create configmap app-config --from-literal=LOG_LEVEL=debug -o yaml --dry-run=client | kubectl apply -f -configmap/app-config configured$ kubectl delete pod cfg; kubectl run cfg --image=busybox:1.36 --restart=Never --overrides='{"spec":{"containers":[{"name":"cfg","image":"busybox:1.36","command":["sleep","300"],"envFrom":[{"configMapRef":{"name":"app-config"}}]}]}}'$ kubectl exec cfg -- printenv LOG_LEVELdebug$ kubectl delete pod cfg; kubectl delete configmap app-configpod "cfg" deletedconfigmap "app-config" deleted
Takeaway
Keep non-secret settings in ConfigMaps, inject them, and roll pods when you need env changes now. Config mistakes are production incidents even when the image never changed.