Hardcoding configuration into a container image is one of the fastest ways to turn a simple environment change into a full rebuild-and-redeploy cycle. ConfigMaps solve this by separating configuration from the image itself, letting the same image run identically across dev, staging, and production with only the config differing. Let’s go through everything from basic usage to production patterns for managing ConfigMaps at scale on AWS EKS.
What Is a ConfigMap?
A ConfigMap is a Kubernetes object that stores non-confidential configuration data as key-value pairs. It’s explicitly not for secrets — passwords, API keys, and certificates belong in a Secret object instead (which, despite the name, is only base64-encoded by default, not encrypted at rest without additional configuration — more on that below).
ConfigMaps can be consumed by pods in three main ways:
- As environment variables
- As command-line arguments
- As files mounted into the container’s filesystem via a volume
Creating a ConfigMap
Imperatively, from literals:
kubectl create configmap app-config \
--from-literal=LOG_LEVEL=info \
--from-literal=API_TIMEOUT_MS=5000 \
-n production
Imperatively, from a file:
kubectl create configmap nginx-config --from-file=nginx.conf -n production
Declaratively (the recommended approach for anything in production):
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: production
data:
LOG_LEVEL: "info"
API_TIMEOUT_MS: "5000"
FEATURE_FLAGS: "new-checkout=true,dark-mode=false"
nginx.conf: |
server {
listen 80;
location / {
proxy_pass http://backend:8080;
}
}
Note that data values can be either simple strings or entire multi-line file contents — both are valid, and which one you use determines how the ConfigMap gets consumed (environment variable vs mounted file).
kubectl apply -f app-config.yaml
kubectl get configmap app-config -n production
kubectl describe configmap app-config -n production
Consuming ConfigMaps as Environment Variables
Individual keys:
spec:
containers:
- name: app
image: myapp:1.0
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL
All keys at once with envFrom:
spec:
containers:
- name: app
image: myapp:1.0
envFrom:
- configMapRef:
name: app-config
envFrom is convenient but has a subtle downside: if a key in the ConfigMap isn’t a valid environment variable name (contains a dot or starts with a digit), it’s silently skipped rather than causing an error — worth knowing when debugging a “why isn’t this config value showing up” issue.
Consuming ConfigMaps as Mounted Files
spec:
containers:
- name: nginx
image: nginx:1.25
volumeMounts:
- name: config-volume
mountPath: /etc/nginx/conf.d
volumes:
- name: config-volume
configMap:
name: nginx-config
Each key in the ConfigMap becomes a separate file in the mounted directory, with the key name as the filename and the value as file content. So nginx.conf: |... from the earlier example becomes /etc/nginx/conf.d/nginx.conf inside the container.
To mount only specific keys, or rename them on mount:
volumes:
- name: config-volume
configMap:
name: nginx-config
items:
- key: nginx.conf
path: default.conf
Live Updates: How ConfigMap Changes Propagate
This trips people up constantly: environment variables sourced from a ConfigMap do NOT update automatically when the ConfigMap changes — env vars are injected once at container start. If you update the ConfigMap, you need to restart the pod for env-var-based config to take effect.
Mounted file-based ConfigMaps, however, DO update automatically (with a delay, typically up to the kubelet sync period, around 60 seconds by default) — the kubelet periodically re-syncs mounted ConfigMap volumes without requiring a pod restart. Your application still needs to actually watch the file for changes and reload, though; Kubernetes only updates the file on disk, it doesn’t notify your process.
kubectl edit configmap nginx-config -n production
# wait ~60s, then check inside the pod:
kubectl exec -it nginx-abc123 -n production -- cat /etc/nginx/conf.d/nginx.conf
If you need config changes to trigger an actual rolling restart of pods (common for env-var-based config, or when your app doesn’t hot-reload files), the standard pattern is to include a hash of the ConfigMap content in a pod annotation, forcing a new ReplicaSet on every config change:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
template:
metadata:
annotations:
checksum/config: "{{ sha256sum (toJson .Values.config) }}"
This exact syntax is Helm-specific (a common pattern in Helm charts), but the underlying principle applies everywhere: change something in the pod template when the ConfigMap changes, so Kubernetes recognizes it needs a new rollout. Tools like Reloader (stakater/reloader) automate this without needing Helm:
helm repo add stakater https://stakater.github.io/stakater-charts
helm install reloader stakater/reloader --namespace kube-system
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
annotations:
reloader.stakater.com/auto: "true"
With Reloader installed and this annotation set, any change to a ConfigMap or Secret referenced by the Deployment automatically triggers a rolling restart — no manual intervention needed.
Immutable ConfigMaps
For large clusters, marking ConfigMaps as immutable improves performance (the API server no longer needs to watch them for changes) and prevents accidental in-place edits to config that should be versioned via new objects instead:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-v2
namespace: production
immutable: true
data:
LOG_LEVEL: "info"
With immutable ConfigMaps, the pattern shifts to creating a new ConfigMap (app-config-v2, app-config-v3, …) per change and updating the Deployment to reference the new name — which naturally triggers a rollout, solving the “changes don’t propagate” problem by design rather than needing Reloader-style tooling.
Managing ConfigMaps with Kustomize
Hand-maintaining ConfigMap YAML for every environment gets tedious fast. Kustomize’s configMapGenerator handles this well, and it also auto-generates a content hash suffix, giving you immutability-by-convention:
# kustomization.yaml
configMapGenerator:
- name: app-config
literals:
- LOG_LEVEL=info
- API_TIMEOUT_MS=5000
files:
- nginx.conf
kubectl kustomize .
This generates something like app-config-8f7d6c5b4a, and any Deployment referencing app-config gets automatically rewritten to reference the hashed name — meaning a config change produces a new ConfigMap object and a fresh rollout automatically, without needing Reloader at all.
For environment-specific overlays:
base/
kustomization.yaml
configmap.yaml
deployment.yaml
overlays/
production/
kustomization.yaml
configmap-patch.yaml
staging/
kustomization.yaml
configmap-patch.yaml
# overlays/production/kustomization.yaml
resources:
- ../../base
configMapGenerator:
- name: app-config
behavior: merge
literals:
- LOG_LEVEL=warn
kubectl apply -k overlays/production/
Managing ConfigMaps with Helm
Helm charts typically template ConfigMaps from values.yaml:
# templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-config
data:
LOG_LEVEL: {{ .Values.logLevel | quote }}
API_TIMEOUT_MS: {{ .Values.apiTimeoutMs | quote }}
# values-production.yaml
logLevel: warn
apiTimeoutMs: 3000
helm upgrade --install my-app ./chart -f values-production.yaml -n production
Size Limits and etcd Considerations
ConfigMaps are stored in etcd, and there’s a hard 1MiB size limit per object (etcd’s default max request size). If you’re tempted to stuff a large dataset or binary blob into a ConfigMap, that’s a signal you actually want a PersistentVolume, an S3 object, or a proper database instead. Large numbers of ConfigMaps and frequent updates also add etcd write load cluster-wide — worth being mindful of on very large EKS clusters with hundreds of namespaces.
Secrets vs ConfigMaps — Don’t Mix Them Up
This deserves emphasis: never put sensitive values into a ConfigMap. Even though ConfigMaps and Secrets have nearly identical APIs, Secrets get additional (opt-in, not automatic) protections like encryption at rest via AWS KMS integration on EKS:
aws eks associate-encryption-config \
--cluster-name production-cluster \
--encryption-config '[{"resources":["secrets"],"provider":{"keyArn":"arn:aws:kms:us-east-1:123456789012:key/xxxx"}}]'
Even better for production secrets management: don’t store raw secret values in Kubernetes manifests at all — use External Secrets Operator to sync from AWS Secrets Manager, as shown in the companion Node.js deployment and Kubernetes Secret articles.
Using ConfigMaps as Command-Line Arguments
Beyond env vars and mounted files, ConfigMap values can also feed into a container’s command/args, useful when a binary only accepts flags and doesn’t read environment variables at all:
spec:
containers:
- name: app
image: myapp:1.0
command: ["/app/server"]
args:
- "--log-level=$(LOG_LEVEL)"
- "--timeout=$(API_TIMEOUT_MS)"
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL
- name: API_TIMEOUT_MS
valueFrom:
configMapKeyRef:
name: app-config
key: API_TIMEOUT_MS
The $(VAR_NAME) syntax in args performs variable substitution from the pod’s own environment — so you still declare the ConfigMap-backed env vars, then reference them in args rather than consuming them directly as environment variables in the application.
Multi-Environment ConfigMap Strategy Comparison
Since there are several valid approaches to managing per-environment config, here’s a practical comparison to help decide:
| Approach | Best for | Tradeoff |
|---|---|---|
| Separate ConfigMap per namespace, same key names | Simple apps, few environments | Manual duplication across environments |
Kustomize overlays with configMapGenerator | Teams already using Kustomize/GitOps | Steeper learning curve upfront |
Helm values-<env>.yaml files | Teams already using Helm charts | Requires Helm templating discipline |
| External config service (e.g., AWS AppConfig) | Large orgs, feature flags, gradual rollouts | Additional infrastructure dependency |
For most teams starting out, Kustomize overlays or Helm values files (whichever matches your existing deployment tooling) cover the vast majority of real needs without introducing a new system. Reach for something like AWS AppConfig only once you need capabilities ConfigMaps genuinely can’t provide, like gradual, monitored config rollouts independent of a full pod deployment.
Troubleshooting
# Confirm the ConfigMap exists and has expected keys
kubectl get configmap app-config -n production -o yaml
# Confirm a pod's env vars actually resolved correctly
kubectl exec -it deploy/app -n production -- env
# Confirm mounted config files
kubectl exec -it deploy/app -n production -- ls -la /etc/nginx/conf.d/
# Events for missing ConfigMap references (pod stuck in ContainerCreating)
kubectl describe pod app-abc123 -n production
A missing ConfigMap referenced by a pod (typo in the name, wrong namespace) produces a CreateContainerConfigError or keeps the pod in ContainerCreating with an event like:
Warning FailedMount configmap "app-cofnig" not found
Best Practices
- Use mounted files for configuration your app can hot-reload; use env vars for simple values where a restart-on-change is acceptable.
- Adopt Kustomize’s
configMapGeneratoror immutable ConfigMaps with versioned names to make config changes trigger proper rollouts automatically, rather than relying on manual restarts. - Never store secrets in ConfigMaps, even “just for now” during development — bad habits from dev environments leak into production.
- Keep ConfigMaps well under the 1MiB etcd limit; move large data to object storage or a database.
- Namespace-scope ConfigMaps per environment (dev/staging/prod) rather than trying to use one giant ConfigMap with environment-conditional logic baked into the application.
Summary
ConfigMaps decouple configuration from container images, letting the same image run across environments with different behavior driven purely by config. Environment-variable consumption is simple but static — changes require a pod restart — while mounted-file consumption supports live updates if your application watches for file changes. Tools like Kustomize’s generator, Helm’s templating, or Reloader all solve the same underlying problem — making sure a config change actually reaches running pods — just with different tradeoffs. And always remember: ConfigMaps are for non-sensitive data only; secrets belong in properly encrypted Secret objects or a dedicated secrets manager.