Baking configuration and credentials directly into a container image means rebuilding the image every time a value changes, and it means secrets end up in your image layers. ConfigMaps and Secrets exist to separate configuration from the image itself.
ConfigMaps — for non-sensitive configuration
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: "info"
MAX_CONNECTIONS: "100"
FEATURE_FLAG_NEW_UI: "true"Secrets — for sensitive values
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
data:
DATABASE_PASSWORD: cG9zdGdyZXNfcGFzc3dvcmQ= # base64-encoded# Easier than hand-encoding base64 yourself
kubectl create secret generic app-secrets \
--from-literal=DATABASE_PASSWORD=postgres_password| Aspect | ConfigMap | Secret |
|---|---|---|
| Purpose | Non-sensitive configuration | Sensitive values (passwords, tokens, keys) |
| Storage | Plain text | Base64-encoded — not encrypted by default |
| Safe to commit to git? | Yes, generally | No — even encoded, it's a reversible real credential |
Using either as environment variables
spec:
containers:
- name: app
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secretsenvFrom injects every key from the ConfigMap/Secret as an environment variable in one shot — useful when you have many values and don't want to list each individually.
Or mounted as files
spec:
containers:
- name: app
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: app-configEach key becomes a file in the mounted directory, with the value as its contents — the right choice for applications that expect configuration as files (like a .env file or an nginx config) rather than environment variables, or for large values environment variables aren't well-suited to.
The important caveat: Secrets aren't encrypted by default
kubectl get secret app-secrets -o yaml shows the value base64-encoded, not encrypted — base64 is an encoding, trivially reversible (echo "..." | base64 -d), not a security measure. Anyone with kubectl access to read Secret objects in that namespace can decode them instantly.
Real protection requires layering on top of the base Secret object:
- Encryption at rest — configuring the cluster's API server with an encryption provider so Secrets are actually encrypted in etcd, not just base64-encoded.
- RBAC — restricting who can read Secret objects via Kubernetes' role-based access control, so "base64 isn't encryption" matters less if far fewer people/services can read the object in the first place.
- External secret managers — tools like AWS Secrets Manager, HashiCorp Vault, or Google Secret Manager integrated via an operator (e.g., External Secrets Operator), so the actual sensitive value lives in a purpose-built secret store, and Kubernetes only holds a reference or a short-lived synced copy.
A reasonable default approach
For low-sensitivity config: ConfigMaps, without a second thought. For genuinely sensitive values (database credentials, API keys, tokens): native Secrets are fine for getting started, but as soon as multiple people have cluster access or the values are genuinely high-stakes, layer on RBAC restrictions and consider an external secret manager rather than treating Secrets as sufficient protection on their own.
Immutable ConfigMaps and Secrets for safer rollouts
Kubernetes supports marking a ConfigMap or Secret immutable: true, which prevents any further edits to that object — sounds counterproductive, but it solves a real problem: the kubelet doesn't need to keep watching an immutable object for changes, reducing API server load on large clusters, and it protects against an accidental edit silently changing behavior for every pod referencing it.
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-v2
data:
LOG_LEVEL: "info"
immutable: trueThe practical pattern that goes with this: instead of editing a ConfigMap in place, create a new versioned one (app-config-v2, app-config-v3) and update the Deployment to reference the new name. Combined with a rolling update, this naturally forces a real pod restart to pick up the new values — solving the "editing a ConfigMap doesn't restart pods automatically" problem from the mistakes list below as a side effect of the versioning pattern itself, rather than needing a separate restart step.
Projecting multiple sources into one volume
A projected volume combines a ConfigMap, a Secret, and other sources into a single mounted directory, useful when an application expects all of its configuration and credentials as files in one place rather than scattered across separate mount points:
volumes:
- name: combined-config
projected:
sources:
- configMap:
name: app-config
- secret:
name: app-secretsThis is a purely organizational tool — it doesn't change how ConfigMaps or Secrets are created or managed, just how they're presented to the container's filesystem, which matters for applications (like some off-the-shelf tools) that expect a single config directory rather than assembling values from multiple mount paths themselves.
Once config and secrets are wired up, the deployment mechanism that actually ships changes matters just as much — Argo CD and GitOps is the pattern most teams reach for to keep these manifests reconciled automatically, and a crash right after a bad ConfigMap change is exactly the kind of thing debugging CrashLoopBackOff walks through.
Generating from a file instead of literal values
For a whole config file (an nginx config, a .env file) rather than a handful of key-value pairs, generating the ConfigMap directly from the file avoids retyping its contents into YAML by hand:
kubectl create configmap nginx-config --from-file=nginx.conf
kubectl create secret generic tls-cert --from-file=tls.crt --from-file=tls.keyThis is the same underlying object as a hand-written YAML manifest — kubectl create ... --from-file just generates it from existing content instead of requiring the value to be typed inline, which matters most for larger config files where a copy-paste into a YAML string would be error-prone.
Common mistakes
- Treating base64 encoding as if it were encryption, and concluding Secrets alone are "secure enough" without RBAC or encryption at rest layered on top.
- Committing a Secret manifest with real values (even base64-encoded) into version control — it's permanently in the git history from that point forward, recoverable by anyone with repo access, decode included.
- Putting genuinely large configuration files (not just a few key-value pairs) into environment variables via
envFrominstead of mounting them as files — some applications and tools expect a real config file on disk, and very large environment variables can hit practical size limits. - Forgetting that updating a ConfigMap/Secret doesn't automatically restart pods using it as an environment variable — pods need to be restarted (or use a mounted-file approach with a reload mechanism) to actually pick up the new values, since
envFromvalues are set once at container start.
Related reading
- How to Debug CrashLoopBackOff in Kubernetes — shares tags: kubernetes, devops, cloud (same category).
- CI/CD Pipelines Explained: From Commit to Production — shares tags: devops, cloud.
- Understanding Cloud Cost Optimization Basics — shares tags: cloud, devops.
- Infrastructure as Code: Why Terraform Won — shares tags: devops, cloud.
- Argo CD and GitOps: Continuous Delivery for Kubernetes — shares tags: devops, kubernetes.