CoursesKubernetes fundamentalsConfigMaps: settings outside the image

ConfigMaps: settings outside the image

One image, many environments.

Beginner10 min · lesson 14 of 24
In plain terms
A ConfigMap is the settings sheet taped to a machine — same machine everywhere, different settings per location. You change the sheet, never the machine.

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

app-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: "info"
app.conf: |
server_port = 8080
timeout = 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.

apply the ConfigMap
kubectl apply -f app-config.yaml
output
configmap/app-config created
check it exists
kubectl get configmap app-config
output
NAME DATA AGE
app-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.

web-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 1
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.27
envFrom:
- configMapRef:
name: app-config
volumeMounts:
- name: cfg
mountPath: /etc/app
volumes:
- name: cfg
configMap:
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.

deploy it
kubectl apply -f web-deploy.yaml
output
deployment.apps/web created
read both values from inside the Pod
kubectl exec deploy/web -- printenv LOG_LEVEL
kubectl exec deploy/web -- cat /etc/app/app.conf
output
info
server_port = 8080
timeout = 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.

broken.yaml
apiVersion: v1
kind: Pod
metadata:
name: broken
spec:
containers:
- name: app
image: nginx:1.27
envFrom:
- configMapRef:
name: does-not-exist
apply it
kubectl apply -f broken.yaml
output
pod/broken created

Now list the Pod.

check on it
kubectl get pod broken
output
NAME READY STATUS RESTARTS AGE
broken 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.

ask why
kubectl describe pod broken
output (events section)
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 20s default-scheduler Successfully assigned default/broken to node-1
Normal Pulled 6s (x3 over 19s) kubelet Container image "nginx:1.27" already present on machine
Warning 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.

make a change take effect
kubectl rollout restart deployment/web
output
deployment.apps/web restarted
You changed a ConfigMap. Does the Pod see it?
ConfigMap updated
how did the value get into the Pod?
as an environment variable
restart the Pod (kubectl rollout restart) to pick it up
frozen when the container started
as a mounted file
takes effect only if the app re-reads the file
kubelet refreshes it after about a minute
Env-var config is a startup snapshot and needs a restart. File-mounted config updates by itself, but the app still has to re-read it.

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.

terminal
$ kubectl create configmap app-config --from-literal=LOG_LEVEL=info
configmap/app-config created
$ kubectl run cfg --image=busybox:1.36 --restart=Never --env=LOG_LEVEL=unused --command -- sleep 300
pod/cfg created
$ kubectl set env pod/cfg --from=configmap/app-config
pod/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_LEVEL
info
$ 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_LEVEL
debug
$ kubectl delete pod cfg; kubectl delete configmap app-config
pod "cfg" deleted
configmap "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.

Quick check
01You edit a ConfigMap value that a running Pod reads as an environment variable. What happens to that Pod?
Incorrect — No. Environment variables are set once, when the container starts, so a running process keeps the old value.
Correct — Env vars are read only at startup, so you roll the workload with kubectl rollout restart for the new value to take effect.
Incorrect — No. Editing a ConfigMap doesn't touch the running Pod, so there's nothing there to crash it.
Incorrect — No. Updating a ConfigMap only stores the new data; it never restarts Pods on its own.
02web-deploy.yaml wires app-config into the container with both envFrom and a volume mount. LOG_LEVEL appears as an environment variable but app.conf does not. Why?
Incorrect — Size is not the reason; the block is small, and the issue is the key's name.
Incorrect — envFrom reads every key that is a valid variable name, not just the first.
Incorrect — The two paths are independent; a mount does not stop envFrom from reading the same key.
Correct — LOG_LEVEL is a legal variable name so it comes through, while app.conf's dot disqualifies it (it is exposed as a file instead).
03A Pod that references a ConfigMap is stuck at CreateContainerConfigError, and kubectl describe shows the event 'configmap "app-config" not found'. What is happening, and what occurs once you create that ConfigMap?
Correct — the container cannot build its environment from a missing ConfigMap, but the kubelet keeps retrying and starts the Pod as soon as the ConfigMap appears.
Incorrect — The event names the ConfigMap, not the image; describe even shows the image was already present.
Incorrect — No manual delete is required; the kubelet retries automatically once the ConfigMap exists.
Incorrect — 'not found' means the object is missing entirely, not that it is empty.
ConfigMaps are plain text, so keep passwords out
Anyone who can read a ConfigMap sees every value in it as plain text, with no encryption at all. That's fine for log levels, web addresses, and other non-secret settings, but it's the wrong home for passwords, API keys, or tokens. Those go in Secrets, which you'll meet in the next lesson. And remember: values injected as environment variables are frozen at startup, so a ConfigMap edit needs a Pod restart to take hold.

Related