ConfigMaps, Secrets, and Environment Injection
What This Concept Is
Configuration should not live in the image. A container image is the code; configuration is everything that varies by environment (log level, database URL, feature flags, credentials). Kubernetes provides two objects for that separation:
- ConfigMap: non-sensitive key/value data. Plain text in etcd.
- Secret: sensitive key/value data. Stored base64-encoded in etcd; optionally encrypted at rest; marked sensitive by RBAC conventions and by most UIs.
Both can be consumed by a Pod in three ways:
- Environment variables (
env.valueFrom.configMapKeyReforsecretKeyRef). - Volume mounts (
volumes: - configMap:or- secret:), which project each key as a file. - envFrom (
envFrom: - configMapRef:orsecretRef:), which injects every key as an env var.
Volume-mounted ConfigMaps and Secrets are updated in place when the object changes (with a kubelet sync delay). Env-var injections are not updated; the Pod must be restarted.
Why It Matters Here
The most common "works on my laptop" bug in Kubernetes is baking environment values into the image. You then end up rebuilding the image to change a log level or point at a different database. ConfigMaps and Secrets make config a first-class, reviewable resource that can be changed independently of the code.
The most common security bug is putting credentials in a ConfigMap because "it is just env vars anyway." Secrets have different default handling (redaction in logs, separate RBAC verbs, optional encryption-at-rest), and some cluster controls apply only to Secrets.
Concrete Example
A ConfigMap and a Secret consumed by one Pod:
apiVersion: v1
kind: ConfigMap
metadata:
name: web-config
data:
LOG_LEVEL: "info"
nginx.conf: |
server { listen 80; root /usr/share/nginx/html; }
---
apiVersion: v1
kind: Secret
metadata:
name: db-creds
type: Opaque
stringData:
DB_USER: "app"
DB_PASS: "s3cret"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 1
selector: { matchLabels: { app: web } }
template:
metadata: { labels: { app: web } }
spec:
containers:
- name: app
image: nginx:1.27
envFrom:
- configMapRef: { name: web-config }
- secretRef: { name: db-creds }
volumeMounts:
- { name: nginx-conf, mountPath: /etc/nginx/conf.d/ }
volumes:
- name: nginx-conf
configMap:
name: web-config
items:
- { key: nginx.conf, path: default.conf }
At runtime the container sees LOG_LEVEL, DB_USER, DB_PASS as env vars, and /etc/nginx/conf.d/default.conf as a file sourced from the ConfigMap.
Common Confusion / Misconception
"Secrets are encrypted."
By default, Secret data is base64-encoded, not encrypted. It is encrypted at rest in etcd only if the cluster was started with EncryptionConfiguration. Even then, anyone with get secrets RBAC can read plaintext. Treat Secrets as a boundary that controls who can read the data, not one that guarantees the data is hidden from cluster operators. For strong secrets, integrate with an external KMS (Vault, cloud secret manager) via projected volumes or CSI drivers.
A second confusion: "Changing a ConfigMap restarts pods that use it." It does not. Volume-mounted ConfigMaps are updated in place (eventually) inside running pods; env-var references are frozen at Pod start. A common safe pattern is to stamp an annotation containing a config hash onto the Deployment template, so changing the ConfigMap triggers a rollout.
A third confusion: "stringData is insecure, data is secure." They are the same. stringData is a convenience that base64-encodes for you; the stored form is identical.
How To Use It
Rules of thumb:
- Everything runtime-configurable belongs in a ConfigMap or a Secret, not baked into the image.
- Credentials go in Secrets even if no mechanism currently enforces the difference.
- Files (certs, config blobs) prefer volume mounts; flags and small strings prefer env vars.
- Decide per-Pod whether config changes should hot-reload (volume) or require a rollout (env).
Check Yourself
- What is the actual storage format of a Secret in etcd by default?
- Why is an env-var reference to a ConfigMap not updated when the ConfigMap changes?
- When would you choose a projected file over an env var?
Triggering a Rollout on Config Change
A common pattern to get env-var consumers to re-read config is annotation stamping:
apiVersion: apps/v1
kind: Deployment
metadata: { name: web }
spec:
template:
metadata:
annotations:
checksum/config: "{{ sha256 .Files.Get \"values.yaml\" }}"
(If you use Helm, Kustomize, or a CI script, compute the hash of the ConfigMap data there and plug it in.)
When the ConfigMap data changes, the hash changes, the Pod template changes, the Deployment controller creates a new ReplicaSet, and Pods roll. No manual restart.
Mini Drill or Application
Apply the manifests above to a cluster. Shell into the pod:
kubectl exec -it deploy/web -- sh
env | grep -E 'LOG_LEVEL|DB_'
cat /etc/nginx/conf.d/default.conf
Edit the ConfigMap's nginx.conf and reapply. Wait 60 seconds, re-read the file in the pod. Did it change? Now change LOG_LEVEL and observe that the env var does not change until the pod restarts. Write a paragraph explaining the update semantics difference.
External Secret Managers: When ConfigMaps/Secrets Aren't Enough
For high-trust credentials (production DB passwords, signing keys, cloud IAM tokens) native Secrets are usually not enough, because:
- Every user with
get secretsRBAC sees plaintext. - Backups of etcd contain the same data.
- Rotation is manual unless you build automation around it.
The production pattern is to source from an external KMS or secret manager and sync or project into the cluster:
- External Secrets Operator watches a
SecretStoreand creates in-cluster Secrets from AWS Secrets Manager, Vault, GCP Secret Manager, Azure Key Vault, etc. - Secrets Store CSI Driver mounts the external secret as a projected file volume -- the data never becomes a full Kubernetes Secret.
- Vault Agent Injector injects credentials as files via a sidecar, with lease-based rotation handled by Vault.
Choose one per cluster and document the rotation story in an ADR. "We store secrets in a Secret" is fine for dev; production needs a story for rotation, audit, and blast radius.
Read This Only If Stuck
- Linux Command Line: What is stored in the environment -- env-var injection becomes obvious once the shell environment model is clear.
- Linux Command Line: Reading, writing, and executing (permissions) -- Secret volume files land with specific UID/GID and mode; these are the primitives.
- Kubernetes: ConfigMaps -- canonical definition and consumption modes.
- Kubernetes: Secrets -- types, base64, and the etcd-encryption option.
- Kubernetes: Encrypting Secret Data at Rest -- the
EncryptionConfigurationsetup that most small clusters skip. - Kubernetes: Configure a Pod to Use a ConfigMap -- all three consumption modes side by side.
- Kubernetes: Projected Volumes -- how multiple sources (ConfigMap, Secret, ServiceAccount token, downward API) compose into one mount point.
- External Secrets Operator -- sync from AWS/Vault/GCP/Azure into cluster Secrets with custom CRDs.
- Secrets Store CSI Driver -- project external secrets as files without creating cluster Secrets.
- HashiCorp Vault: Kubernetes integration -- agent sidecar, auth methods, and dynamic credentials.