Kubernetes ConfigMaps and Secrets Tutorial
A practical walkthrough of ConfigMaps and Secrets in Kubernetes, including how to inject them as environment variables, mount as files, and rotate safely.
What you'll learn
- ✓When to use a ConfigMap vs a Secret
- ✓Injection as env vars vs volume mounts
- ✓How rotation actually works in running pods
- ✓Encryption at rest and external secret stores
- ✓Common security and reload pitfalls
Prerequisites
- •Working knowledge of Pods and Deployments
What and Why
Twelve-factor apps separate code from configuration. In Kubernetes, that separation lives in two objects: ConfigMap for non-sensitive data and Secret for sensitive data. Both look similar on the surface — key/value pairs stored in etcd, injected into pods — but they have different defaults around encoding, access control, and storage encryption.
Getting this right keeps secrets out of your container images and makes config changes possible without rebuilding.
Mental Model
Think of ConfigMaps and Secrets as small in-cluster key/value stores. Pods reference them in two ways:
- As environment variables: values are baked into the pod at start time. Updates require a pod restart.
- As volume mounts: values appear as files inside the container. The kubelet refreshes them periodically, so updates can propagate live.
[ConfigMap] [Secret]
| |
+----------+----------+
|
v
[Pod spec]
/ \
envFrom volumeMounts
| |
[env vars set [/etc/config/*]
at startup] (refreshed by kubelet) Hands-on Example
Create a ConfigMap from literals or files:
kubectl create configmap app-config \
--from-literal=LOG_LEVEL=info \
--from-literal=FEATURE_FLAG_X=true
kubectl create configmap nginx-config \
--from-file=nginx.conf=./nginx.conf
Create a Secret similarly:
kubectl create secret generic db-creds \
--from-literal=username=appuser \
--from-literal=password='s3cr3t!'
Inject both into a Deployment:
apiVersion: apps/v1
kind: Deployment
metadata: { name: api }
spec:
replicas: 2
selector: { matchLabels: { app: api } }
template:
metadata: { labels: { app: api } }
spec:
containers:
- name: api
image: myorg/api:1.4
envFrom:
- configMapRef: { name: app-config }
- secretRef: { name: db-creds }
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/conf.d
readOnly: true
volumes:
- name: nginx-conf
configMap:
name: nginx-config
envFrom exposes every key as an environment variable. The volume mount makes nginx.conf appear as a file.
Common Pitfalls
Secrets are base64, not encrypted. By default, etcd stores secrets base64-encoded. Anyone with cluster access can decode them. Enable encryption at rest via an EncryptionConfiguration on the API server, and use cloud KMS as the provider.
Env-var secrets leak into logs. A misbehaving framework that dumps process.env on startup will print your DB password. Prefer file mounts for high-sensitivity values.
No automatic restart on change. Updating a ConfigMap does not restart pods that reference it via envFrom. Either roll the Deployment manually or use a controller like Reloader, or compute a hash of the config and put it in a pod annotation so changes force a rollout:
spec:
template:
metadata:
annotations:
checksum/config: "{{ .Values.configChecksum }}"
Size limits. ConfigMaps and Secrets are capped at roughly 1 MiB. Large blobs belong in object storage with a pre-signed URL passed via config, not in etcd.
Cross-namespace references are forbidden. Each pod can only reference ConfigMaps and Secrets in its own namespace. Use a tool like External Secrets Operator if you need a single source of truth across namespaces.
Practical Tips
For production, use an external secret store and sync into Kubernetes:
- AWS Secrets Manager + External Secrets Operator
- HashiCorp Vault Agent injector
- GCP Secret Manager CSI driver
These keep the source of truth out of etcd, give you audit logs, and rotate credentials automatically.
To declare secrets in Git without committing plaintext, use Sealed Secrets or SOPS-encrypted manifests. Both encrypt secrets with a cluster-side key that only the controller can decrypt.
When a config change should trigger a rolling restart, run:
kubectl rollout restart deployment/api
This bumps the pod template revision and triggers a normal rollout.
For mounted ConfigMaps, the kubelet sync period is about a minute. Apps that watch their config file (NGINX with inotify, for example) can reload without a restart. Apps that read once at startup cannot.
Always set RBAC carefully on Secrets. A default ServiceAccount with get secrets permission is effectively a credential dump on demand.
Wrap-up
Use ConfigMaps for plain configuration and Secrets for credentials, but treat the latter with real care: base64 is not encryption, env vars leak, and updates need a deliberate rollout. Mount secrets as files when possible, hash your config into pod annotations to trigger rollouts on change, and graduate to an external secret store before your team grows past a handful of services. Treat config as deployment metadata, version it like code, and you will avoid the most common cluster outages.
Related articles
- Kubernetes Kubernetes Secrets: Best Practices for Production
Practical patterns for managing Kubernetes Secrets safely: encryption at rest, external secret stores, RBAC scoping, rotation, and avoiding common leaks.
- Kubernetes Kubernetes Network Policies: A Practical Tutorial
Learn how Kubernetes NetworkPolicies work, how to lock down pod-to-pod traffic with selectors, and how to roll out a default-deny posture without breaking your cluster.
- Kubernetes Kubernetes RBAC Cheatsheet: Roles, Bindings, and Service Accounts
A concise tour of Kubernetes RBAC: roles vs cluster roles, bindings, service accounts, and patterns that scale without becoming a permissions zoo.
- CI/CD CI/CD Secrets Management Best Practices
Keep API keys, tokens, and database credentials safe in CI/CD with rotation, scoping, secret managers, and OIDC-based authentication.