ConfigMaps: settings outside the image
One image, many environments.
The same container image should run in more than one environment. Settings that change per environment — hostnames, feature flags, non-secret ports — belong outside the image. A ConfigMap holds those values. You do not rebuild the image to change them.
A container image is a frozen, ready-to-run copy of the app and what it needs to start. You build it once and ship that copy everywhere. An environment is one of those places: laptop, a shared test server, or production. Settings that differ between environments — which database to connect to, how chatty the logs are — should not be baked into the image. Bake them in and you rebuild for every log-level change. In Kubernetes those values live in a ConfigMap: key/value pairs (for example LOG_LEVEL set to info) handed to the 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
A ConfigMap reaches the container two ways, and the difference matters in a minute. Environment variables are handed to the process at start. The other way is files: each key becomes a file the app can open later. Some apps want env vars. Others want a file at a path like /etc/app/app.conf. A ConfigMap can do both.
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.
Ship the wrong database hostname in a ConfigMap and production still breaks: 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.