ConfigMap
A ConfigMap is a Kubernetes API object that stores non-confidential configuration as key-value pairs, decoupling configuration from container images so the same image can run unchanged in dev, staging, and production with different configuration mounted at runtime. The Kubernetes documentation defines it: “A ConfigMap is an API object used to store non-confidential data in key-value pairs. Pods can consume ConfigMaps as environment variables, command-line arguments, or as configuration files in a volume.” (k8s.io — ConfigMaps). The pattern it formalizes is the twelve-factor “Config in the environment” rule — application code reads its config from the environment or files, the platform supplies them, and the image is portable. ConfigMaps are deliberately unencrypted: anyone with read access to the namespace can read every value. Anything secret must instead go into a Secret (which, despite the name, is also only base64-encoded — see that note). The size ceiling is 1 MiB per ConfigMap (k8s.io docs), enforced because every object lives in etcd and very large objects degrade the kube-apiserver’s watch performance for everyone. The two ways to consume one — as environment variables vs. as files mounted from a volume — have materially different update semantics, and getting that distinction wrong is the leading cause of “I rolled out new config and nothing changed” support tickets.
Mental Model
flowchart LR subgraph CM["ConfigMap: my-app-config"] K1["data:<br/> LOG_LEVEL: info<br/> REGION: us-east-1<br/> config.yaml: |<br/> cache:<br/> ttl: 60s"] K2["binaryData:<br/> cert.der: aGVsbG8K..."] end subgraph POD["Pod: my-app"] subgraph CTR["Container"] ENV["Env vars<br/>(frozen at start)<br/>LOG_LEVEL=info<br/>REGION=us-east-1"] FS["Volume mount<br/>(updated atomically)<br/>/etc/config/LOG_LEVEL<br/>/etc/config/REGION<br/>/etc/config/config.yaml"] end end CM -- "envFrom.configMapRef<br/>or valueFrom.configMapKeyRef" --> ENV CM -- "volumes.configMap +<br/>volumeMounts" --> FS
What this diagram shows. A single ConfigMap can be consumed in both modes by the same container, and the two modes are radically different in their update behavior. The env-var path stamps each key into the container’s environment at process start — the kernel execve() arguments are fixed for the process’s lifetime, and Kubernetes does not (and cannot, in general) edit a running process’s environment. The volume-mount path writes each key as a file at the mount path, and the kubelet watches the API server for updates; when the ConfigMap is edited, the mounted files are atomically re-projected. The insight to extract: if you want live config reloads, mount as a volume; if you want a startup-only snapshot (the common case), inject as env vars. Mixing the two for the same key inside the same container is a common source of confusion — the env var holds the old value, the file holds the new value, the app reads one or the other and you can’t tell which.
Mechanical Walk-through
What a ConfigMap actually is
A ConfigMap is a namespaced API resource with three notable fields:
data— amap[string]stringof UTF-8 key-value pairs. Keys are filenames if mounted (must follow filename rules); values are arbitrary strings (frequently entire JSON/YAML/TOML/properties files inlined as one long string).binaryData—map[string][]byte(base64-encoded over the wire) for binary content like certificates or compiled assets. Added in K8s 1.10. Keys indataandbinaryDatamust not overlap.immutable—bool, added GA in K8s 1.21 (KEP-1412). Whentrue, the API server rejects any mutation other than deletion — the only way to “change” the ConfigMap is to create a new one with a different name and re-roll Pods.
The 1 MiB limit is documented explicitly: “The data stored in a ConfigMap cannot exceed 1 MiB. If you need to store settings that are larger than this limit, you may want to consider mounting a volume or use a separate database or file service.” (k8s.io — ConfigMaps). The limit is rooted in the etcd default object-size limit (1.5 MiB after metadata overhead), and is shared with Secret.
Consumption mode 1: as environment variables
There are two sub-flavors:
Single key via valueFrom.configMapKeyRef:
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: my-app-config
key: LOG_LEVEL
optional: false # if true and key missing, env var simply unsetAll keys via envFrom.configMapRef:
envFrom:
- configMapRef:
name: my-app-config
prefix: APP_ # optional; namespaces every key to avoid collisionsTwo crucial properties:
- Values are frozen at container start. The kubelet reads the ConfigMap at the moment it starts the container; the resulting env vars are baked into the process’s environment via the OCI runtime spec. If the ConfigMap is updated later, the env var inside the running container does not change. To pick up new values: restart the Pod (or do a rollout via the Deployment).
- Missing ConfigMap or missing key prevents Pod start. Unless
optional: true, the kubelet refuses to start the container until the ConfigMap exists and contains the key. This shows up asCreateContainerConfigErrorinkubectl describe pod. The same applies toenvFrom(where the entire ConfigMap must exist) — thoughoptional: trueat theenvFromlevel skips missing ConfigMaps silently.
Consumption mode 2: as files in a volume
volumes:
- name: app-config
configMap:
name: my-app-config
defaultMode: 0644 # POSIX permissions on the projected files
items: # optional: cherry-pick or rename
- key: config.yaml
path: cache/config.yaml
mode: 0600
containers:
- name: app
volumeMounts:
- name: app-config
mountPath: /etc/config
readOnly: true # almost always true; mounted CMs are RO anywayThe kubelet projects each key in the ConfigMap as a file at <mountPath>/<key>, with the value as the file’s content. The mount is a tmpfs (memory-backed) — no node-disk I/O. The projection happens during Pod startup before the container starts, and is refreshed periodically as the ConfigMap changes.
Update propagation. When the ConfigMap is edited via the API server:
- The kubelet on each node where the Pod runs watches the ConfigMap via the standard list-watch mechanism.
- When a change is observed, the kubelet re-projects the files in the tmpfs mount atomically (using a rename swap: write the new content into a sibling directory, atomically rename it into place, delete the old). At no point can the application read a half-written file.
- The K8s docs note the propagation is eventually consistent, with a latency bounded by the kubelet’s sync period (default 1 minute) plus the API server’s watch latency. In practice, updates land within tens of seconds to a couple of minutes.
This is the crucial update-semantics difference: volume-mounted ConfigMaps reflect new values without a Pod restart; env-var-mounted ConfigMaps do not. The K8s docs are explicit: “ConfigMaps consumed as environment variables are not updated automatically and require a pod restart.”
Edge case: subPath defeats updates
If you mount a single key into a host-path-style location using subPath:
volumeMounts:
- name: app-config
mountPath: /etc/app/config.yaml
subPath: config.yamlthen updates are not propagated. subPath creates a one-shot file copy at container start, not an atomic re-projection. This is documented but easy to miss; users who need both “mount this single file at a non-conflicting path” and “live updates” must instead mount the whole ConfigMap into a directory and symlink/reference the file from there.
Immutability (GA in 1.21)
Setting immutable: true on a ConfigMap signals to the API server and the kubelet that the object will never be edited. The performance benefit is large at scale: the kubelet no longer watches that ConfigMap (it can serve the value from local cache forever), reducing watch-stream pressure on the API server. The KEP-1412 design frames the motivation: clusters with thousands of Pods each mounting a ConfigMap can cause significant kubelet→apiserver watch traffic, and most of that watching is wasted because the ConfigMaps don’t change.
Operational pattern with immutable ConfigMaps: hash the content into the name (my-config-a3f7d1c8), reference the hashed name from the Pod template, and re-roll Pods on configuration change (Kustomize’s configMapGenerator does this automatically).
Built-in defaults and reserved keys
The ConfigMap API doesn’t reserve any keys. However, several Kubernetes-internal ConfigMaps live in the kube-system namespace and follow conventions: kube-root-ca.crt (one per namespace, contains the cluster CA bundle, automatically projected into Pods); cluster-info (used by kubeadm for join); coredns (CoreDNS Corefile). Be aware that any ConfigMap you create in kube-system looks like cluster infrastructure to operators; keep your application config in application namespaces.
Configuration / API Surface
A ConfigMap with mixed-shape config
apiVersion: v1
kind: ConfigMap
metadata:
name: my-app-config
namespace: payments
data:
# Simple key-value (good for env-var consumption)
LOG_LEVEL: info
REGION: us-east-1
FEATURE_FLAG_NEW_PRICING: "true" # YAML quoting matters: bool would become "true" string anyway,
# but other types need explicit quoting
# File-shaped config (good for volume mounting)
application.properties: |
server.port=8080
cache.ttl=60s
cache.maxSize=1000
logback.xml: |
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder><pattern>%d %p %c %m%n</pattern></encoder>
</appender>
<root level="INFO"><appender-ref ref="STDOUT"/></root>
</configuration>
binaryData:
# Base64-encoded over the wire; kubelet base64-decodes when projecting
custom-truststore.jks: |
/u3+7QAAAAIAAAAB...A Pod consuming the ConfigMap both ways
apiVersion: v1
kind: Pod
metadata:
name: my-app
namespace: payments
spec:
containers:
- name: app
image: my-app:v1.4.2
# Env-var mode: frozen at start, requires restart for updates
envFrom:
- configMapRef:
name: my-app-config
env:
- name: SPECIFIC_KEY
valueFrom:
configMapKeyRef:
name: my-app-config
key: REGION
# Volume mode: live-updated (except subPath mounts)
volumeMounts:
- name: config-files
mountPath: /etc/app
readOnly: true
volumes:
- name: config-files
configMap:
name: my-app-config
items:
- key: application.properties
path: application.properties
- key: logback.xml
path: logback.xmlImmutable variant
apiVersion: v1
kind: ConfigMap
metadata:
name: my-app-config-a3f7d1c8 # content hash in name
namespace: payments
data:
LOG_LEVEL: info
immutable: true # API server rejects edits;
# kubelet skips watching for changesOperational commands
# Create from a literal
kubectl create configmap my-app-config --from-literal=LOG_LEVEL=info -n payments
# Create from a file (whole file becomes a single key)
kubectl create configmap my-app-config --from-file=application.properties -n payments
# Create from a directory (each file becomes a key)
kubectl create configmap my-app-config --from-file=./conf/ -n payments
# Edit (only allowed if immutable: false)
kubectl edit configmap my-app-config -n payments
# Force a rollout when consuming via env vars (no auto-restart on edit)
kubectl rollout restart deployment my-app -n paymentsFailure Modes
CreateContainerConfigError: ConfigMap or specified key is missing andoptional: false. The Pod is stuck in this state. Diagnosis:kubectl describe pod. Fix: create the ConfigMap or fix the key reference; the Pod recovers automatically.- Edited ConfigMap, app didn’t pick up changes. Almost always: app consumes via env var (which is frozen at start). Fix:
kubectl rollout restart. Alternative: switch to volume-mount consumption. - Edited ConfigMap mounted via
subPath, app didn’t pick up changes.subPathdefeats live updates. Fix: mount the whole ConfigMap as a directory; or restart Pods. - ConfigMap exceeds 1 MiB. API server rejects with
ConfigMap "..." is invalid: data: Too long: must have at most 1048576 bytes. Fix: split into multiple ConfigMaps, or use a different mechanism (init container that downloads from object storage, externalized config server, etc.). - Concurrent rollouts with shared mutable ConfigMap. If two Deployments use the same ConfigMap and one is edited mid-rollout, Pods in different states see different values. Fix: prefer immutable ConfigMaps with hashed names, one per release.
- ConfigMap name in a
kube-system-style sensitive namespace. Some admission policies (Pod Security Admission, custom Gatekeeper rules) restrict what can be created in privileged namespaces. Application ConfigMaps live in application namespaces.
Alternatives and When to Choose Them
- Secret for confidential values. ConfigMap does not encrypt at rest by default. Anything secret — passwords, API keys, TLS keys — goes in a Secret (which is base64-encoded and can be encrypted-at-rest if the cluster admin configures etcd Encryption at Rest). RBAC on Secret is also stricter by convention.
- Downward API for Pod metadata. ConfigMap is for external config. The Pod’s own name, namespace, node, IP, resource limits come from the Downward API, not from a ConfigMap.
- In-image baked-in config. Sometimes correct: build-time-determined values (
/etc/timezone, default templates) belong in the image. The line between “image content” and “ConfigMap content” is “does this change per environment?” - External config service (Consul, etcd-as-app-config, AWS AppConfig). For dynamic config that changes faster than a Pod rollout cadence and requires application-aware reload logic, an external system with a client-library polling pattern is often better than fighting K8s’s once-per-Pod-or-volume-mount semantics. Tradeoff: extra moving parts.
- GitOps templating with Helm or Kustomize. ConfigMap contents that vary per environment are rendered from Helm values or Kustomize overlays. The ConfigMap itself is just the rendered output; the source of truth is the templating pipeline. Pair with ArgoCD / Flux for automated reconciliation.
- Environment-variable injection from a MutatingAdmissionWebhook. Some platforms inject standard env vars (cluster name, datacenter, dial-home endpoints) via webhook rather than a ConfigMap. Useful for platform-team-controlled values that don’t belong in app teams’ manifests.
Production Notes
- The “configmap hash in name” pattern is industry standard. Kustomize’s
configMapGeneratorcomputes a content hash and appends it to the ConfigMap name; the Deployment’s PodTemplate references the hashed name; editing the ConfigMap content creates a new ConfigMap with a new name, which makes the PodTemplate diff change, which triggers a rolling update. This solves the “I edited the ConfigMap but nothing rolled out” problem definitively. Spotify, Shopify, and most production GitOps shops use this or an equivalent. - Immutable ConfigMaps at scale. A cluster with thousands of Pods each mounting a ConfigMap that never changes (TLS bundle, country code mapping) sees measurable kubelet→apiserver watch traffic that is pure waste. Setting
immutable: trueremoves those watches. Combined with hashed names, you get the best of both worlds: low watch overhead and unambiguous rollout-on-change semantics. - The 1 MiB limit and “config sprawl.” Large applications often grow into “we have 30 ConfigMaps for this service.” Two patterns help: (a) split by update cadence — one ConfigMap per release vs. one for slowly-changing topology data — so re-rolls are scoped; (b) use Projected Volumes to mount multiple ConfigMaps under one directory, hiding the multiplicity from the application.
- Never put secrets in ConfigMaps “temporarily.” A surprising fraction of k8s.af incidents involve credentials accidentally committed to a ConfigMap that then ended up in a Git-backed manifest store. Use Secret from day one, even before etcd Encryption at Rest is enabled, because the RBAC posture and the intent signal matter.
- Watch behavior under high CM churn. A high rate of ConfigMap updates (e.g., a controller continuously writing observed state to a ConfigMap) can pressure the apiserver’s watch cache and cause
Watch closed: 410 Gonereconnects for all watchers. The right place for high-write-rate “configuration-shaped” data is a CRD with astatussubresource, not a ConfigMap.
See Also
- Secret — sibling for confidential data; nearly identical surface, different security expectations
- Downward API — sibling for Pod’s own metadata
- Projected Volume — combine multiple ConfigMaps, Secrets, and Downward API sources into one mount path
- Volume Types (Kubernetes) —
configMapis one of the documented volume types - Pod — the consumer; ConfigMap injection happens at Pod start (env vars) or via kubelet projection (volumes)
- Pod Lifecycle —
CreateContainerConfigErroris one of the lifecycle-phase failures from missing ConfigMaps - etcd — every ConfigMap lives here; the 1 MiB limit derives from etcd’s per-object size
- Kustomize — the canonical templating tool with
configMapGeneratorand hashed-name semantics - Helm — the chart pattern often produces ConfigMaps as part of release-rendered manifests
- 12-Factor App Methodology — the philosophy ConfigMap implements
- Kubernetes MOC §8 — Configuration and Secrets