05 - ConfigMaps and Secrets
Why this matters
You already know from the Docker track not to bake environment-specific
values into an image — that's why docker run -e and .env files exist.
Kubernetes has the same principle at cluster scale: ConfigMaps and
Secrets let the same Pod template (same image, same Deployment YAML) run
in different environments with different configuration, without ever
rebuilding the image. Secrets additionally give you a (mildly) safer way
to handle credentials than plain environment variables baked into a
manifest committed to git.
Concepts
A ConfigMap is just a named bag of key-value data, stored in the
cluster (in etcd, via the API server, exactly like every other object
from module 01). It holds no logic — it's a place to put configuration
so it's decoupled from your Pod template and your image, similar in
spirit to a .env file, except it lives in the cluster instead of on
disk.
A Secret is structurally almost identical to a ConfigMap — same shape, same ways to consume it — but intended for sensitive values (passwords, tokens, keys). The values are stored base64-encoded, which is encoding, not encryption — anyone who can read the Secret object via the API can trivially decode it. Base64 exists so binary data survives transport in YAML/JSON, not for security. Real protection comes from restricting who can read Secret objects at all (RBAC, module 11) and, in production clusters, encrypting etcd at rest — a cluster-admin-level concern, not something you configure per-Secret.
Three ways to consume either one in a Pod:
- As environment variables — one at a time (
env.valueFrom) or all keys at once (envFrom). Simple, but the Pod must be restarted to pick up changes, and env vars are visible inkubectl describe podand process listings inside the container (a mild exposure concern for Secrets). - As mounted files — each key becomes a file in a directory inside the container, with the key's value as file content. Files can update live when the ConfigMap/Secret changes (the kubelet periodically syncs mounted ConfigMaps/Secrets), without a Pod restart, though your application still needs to notice the file changed.
- Referenced directly by other objects — e.g., an
imagePullSecretsentry referencing a Secret holding registry credentials, which is how a Pod authenticates to pull an image from a private registry like Azure Container Registry.
ConfigMap / Secret how the Pod consumes it
┌────────────────┐
│ LOG_LEVEL=debug│──► env vars ──► $LOG_LEVEL in the container
│ password=***** │ (read once at container start)
│ │
│ │──► volume ──► /etc/config/LOG_LEVEL (a file)
│ │ mount /etc/secrets/password (a file)
└────────────────┘ (files can update live, no restart)
env-var values are frozen at start; mounted files re-sync
Why the split into two object types at all, given they're structurally so similar: it's a signal to humans and tooling. RBAC rules (module 11) commonly grant broad read access to ConfigMaps but restrict Secret reads tightly; tools that scan manifests for accidentally-committed credentials look specifically for Secret objects. Using a Secret for anything sensitive — even if a ConfigMap would technically work — is the convention that keeps those protections meaningful.
Immutable ConfigMaps/Secrets (immutable: true) tell the API server
to reject any future updates to the object — useful for config you
version by creating a new object (e.g. app-config-v2) rather than
editing in place, which also lets the kubelet skip watching it for
changes, a minor performance win at cluster scale.
Command reference
| Command | What it does | Example |
|---|---|---|
kubectl create configmap <name> --from-literal=<k>=<v> |
Creates a ConfigMap from literal key/value pairs | kubectl create configmap app-config --from-literal=LOG_LEVEL=debug |
kubectl create configmap <name> --from-file=<path> |
Creates a ConfigMap from a file (filename becomes the key) | kubectl create configmap nginx-conf --from-file=nginx.conf |
kubectl create secret generic <name> --from-literal=<k>=<v> |
Creates a generic Secret from literal values | kubectl create secret generic db-creds --from-literal=password=s3cr3t |
kubectl create secret docker-registry <name> --docker-server=... --docker-username=... --docker-password=... |
Creates a Secret usable as imagePullSecrets |
kubectl create secret docker-registry acr-secret --docker-server=myregistry.azurecr.io --docker-username=... --docker-password=... |
kubectl get configmaps / kubectl get secrets |
Lists ConfigMaps/Secrets | kubectl get cm; kubectl get secrets |
kubectl describe configmap <name> |
Shows keys and (for ConfigMaps) values | kubectl describe cm app-config |
kubectl get secret <name> -o jsonpath='{.data.<key>}' | base64 -d |
Decodes a Secret value for inspection | kubectl get secret db-creds -o jsonpath='{.data.password}' | base64 -d |
envFrom.configMapRef / envFrom.secretRef |
Injects every key in a ConfigMap/Secret as env vars | see exercises |
env[].valueFrom.configMapKeyRef / secretKeyRef |
Injects one specific key as one env var | see exercises |
volumes[].configMap / volumes[].secret |
Mounts a ConfigMap/Secret as files in a volume | see exercises |
spec.imagePullSecrets |
References a docker-registry Secret for pulling private images | imagePullSecrets: [{name: acr-secret}] |
Hands-on exercises
Continue in namespace demo.
1. Create a ConfigMap two ways
kubectl create configmap app-config --from-literal=LOG_LEVEL=debug --from-literal=GREETING="hello from configmap"
kubectl get configmap app-config -o yaml
Expected: a data section with both keys, in plain text (ConfigMaps are
never encoded).
2. Consume it as environment variables (all keys at once)
# pod-cm-env.yaml
apiVersion: v1
kind: Pod
metadata:
name: cm-env-demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "env | grep -E 'LOG_LEVEL|GREETING'; sleep 3600"]
envFrom:
- configMapRef:
name: app-config
kubectl apply -f pod-cm-env.yaml
kubectl logs cm-env-demo
Expected: LOG_LEVEL=debug and GREETING=hello from configmap printed.
3. Consume one specific key
# pod-cm-onekey.yaml
apiVersion: v1
kind: Pod
metadata:
name: cm-onekey-demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "echo \"level is: $MY_LOG_LEVEL\"; sleep 3600"]
env:
- name: MY_LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL
kubectl apply -f pod-cm-onekey.yaml
kubectl logs cm-onekey-demo
Expected: level is: debug — note the env var name (MY_LOG_LEVEL) can
differ from the ConfigMap key name (LOG_LEVEL).
4. Mount a ConfigMap as files
# pod-cm-volume.yaml
apiVersion: v1
kind: Pod
metadata:
name: cm-volume-demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
volumeMounts:
- name: config-vol
mountPath: /etc/app-config
volumes:
- name: config-vol
configMap:
name: app-config
kubectl apply -f pod-cm-volume.yaml
kubectl exec cm-volume-demo -- ls /etc/app-config
kubectl exec cm-volume-demo -- cat /etc/app-config/GREETING
Expected: files named LOG_LEVEL and GREETING, each containing that
key's value.
5. Create and inspect a Secret
kubectl create secret generic db-creds --from-literal=username=admin --from-literal=password='S3cr3tP@ss'
kubectl get secret db-creds -o yaml
Expected: data section shows base64 strings, not plaintext. Decode one:
kubectl get secret db-creds -o jsonpath='{.data.password}' | base64 -d; echo
Expected: S3cr3tP@ss prints — proving base64 is not real protection,
just encoding.
6. Consume a Secret as env vars and as a mounted file
# pod-secret-demo.yaml
apiVersion: v1
kind: Pod
metadata:
name: secret-demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-creds
key: username
volumeMounts:
- name: secret-vol
mountPath: /etc/secrets
readOnly: true
volumes:
- name: secret-vol
secret:
secretName: db-creds
kubectl apply -f pod-secret-demo.yaml
kubectl exec secret-demo -- sh -c 'echo $DB_USERNAME; cat /etc/secrets/password'
Expected: admin then S3cr3tP@ss — both files under
/etc/secrets/ already decoded, not base64.
7. Live-update a mounted ConfigMap without restarting the Pod
kubectl create configmap live-config --from-literal=MESSAGE="version 1"
# pod-live.yaml
apiVersion: v1
kind: Pod
metadata:
name: live-demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "while true; do cat /etc/live/MESSAGE; sleep 5; done"]
volumeMounts:
- name: live-vol
mountPath: /etc/live
volumes:
- name: live-vol
configMap:
name: live-config
kubectl apply -f pod-live.yaml
kubectl logs live-demo --follow &
kubectl patch configmap live-config --type=merge -p '{"data":{"MESSAGE":"version 2"}}'
Expected: within roughly a minute (the kubelet's sync interval), the
looped output changes from version 1 to version 2 with no Pod
restart. Stop following logs with Ctrl+C (or kill %1) when you've
confirmed it.
8. Use a Secret for private registry pulls (as you'd do for ACR)
kubectl create secret docker-registry acr-secret \
--docker-server=myregistry.azurecr.io \
--docker-username=myuser \
--docker-password=mypassword \
--docker-email=me@example.com
kubectl get secret acr-secret -o yaml
Expected: a Secret of type kubernetes.io/dockerconfigjson. Reference it
in a Pod spec (don't actually run this one against a real private image
unless you have one available):
spec:
imagePullSecrets:
- name: acr-secret
containers:
- name: app
image: myregistry.azurecr.io/myapp:1.0
This is exactly what you'll do in the AKS track to pull images from Azure Container Registry — the mechanism doesn't change, only that there you'll typically wire ACR access via AKS's managed identity instead of a manually-created Secret.
9. Diagnose and fix: env var referencing a missing key
# pod-badref.yaml
apiVersion: v1
kind: Pod
metadata:
name: badref-demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
env:
- name: MY_VALUE
valueFrom:
configMapKeyRef:
name: app-config
key: DOES_NOT_EXIST
kubectl apply -f pod-badref.yaml
kubectl get pod badref-demo
Expected: STATUS: CreateContainerConfigError. Diagnose:
kubectl describe pod badref-demo
Expected: an event like Error: couldn't find key DOES_NOT_EXIST in ConfigMap demo/app-config. Fix by correcting the key name to one that
actually exists (LOG_LEVEL or GREETING), re-apply, and confirm
Running.
10. Diagnose and fix: referencing a Secret/ConfigMap in the wrong namespace
kubectl create namespace other
# pod-wrongns.yaml
apiVersion: v1
kind: Pod
metadata:
name: wrongns-demo
namespace: demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
envFrom:
- configMapRef:
name: config-that-lives-in-other-namespace
First create the referenced ConfigMap only in other, not demo:
kubectl create configmap config-that-lives-in-other-namespace --from-literal=X=1 -n other
kubectl apply -f pod-wrongns.yaml
kubectl get pod wrongns-demo -n demo
Expected: CreateContainerConfigError again — ConfigMaps/Secrets are
namespaced objects, invisible across namespace boundaries. Diagnose with
kubectl describe pod wrongns-demo -n demo (event mentions the
ConfigMap not found), fix by creating an equivalent ConfigMap inside
demo itself:
kubectl create configmap config-that-lives-in-other-namespace --from-literal=X=1 -n demo
kubectl delete pod wrongns-demo -n demo
kubectl apply -f pod-wrongns.yaml
kubectl get pod wrongns-demo -n demo
Expected: Running. Clean up this module's objects:
kubectl delete pod cm-env-demo cm-onekey-demo cm-volume-demo secret-demo live-demo badref-demo wrongns-demo -n demo
kubectl delete configmap app-config live-config config-that-lives-in-other-namespace -n demo
kubectl delete secret db-creds acr-secret -n demo
kubectl delete namespace other
Independent challenge
No YAML or commands given here — figure it out yourself using what you know from this module and earlier ones.
Task: Deploy a small web application (a Deployment, not a bare Pod)
whose container reads a non-sensitive setting (say, a LOG_LEVEL) from a
ConfigMap as an environment variable, and reads a database password from a
Secret as a mounted file. Put a Service in front of it. Now change the
LOG_LEVEL value in the ConfigMap and get the running Pods to actually
pick up the new value — reasoning first about whether an env-var
consumer can see the change without a restart, and doing whatever is
required to make it take effect. This combines this module's ConfigMap/
Secret consumption with Deployments (module 03) and Services (module 04).
Stuck? One hint
Env-var values are read only at container start, so a ConfigMap edit
alone won't reach a running Pod — you need to roll the Deployment's Pods
(e.g. kubectl rollout restart deployment/<name>); mounted-file
consumption would have updated on its own.
Common mistakes & troubleshooting
- Treating base64 as encryption: anyone with API read access to a Secret can decode it trivially. Restrict Secret access with RBAC (module 11); don't rely on base64 for confidentiality.
- Referencing a key that doesn't exist: results in
CreateContainerConfigError, not a clear "typo" message — always cross-check the exact key names withkubectl describe configmap/secret <name>. - Forgetting ConfigMaps/Secrets are namespaced: a Pod in namespace
democannot reference a ConfigMap/Secret that only exists in namespaceother— you need one in each namespace that needs it, or a templating tool (Helm, module 07) to keep them in sync. - Expecting env-var consumption to update live: it won't — Pods only read env vars at container start, so a ConfigMap/Secret change requires a Pod restart (a rolling restart of a Deployment) to take effect when consumed as env vars, unlike mounted-file consumption.
- Committing plaintext Secret manifests to git: a Secret's
datais only base64, so a committed manifest is functionally a committed plaintext credential — treat Secret YAML the same as any other credential file (don't commit it; use e.g.kubectl create secretin a pipeline, or a proper secrets-management tool in production).
Checkpoint quiz
Write down your answer to each question before expanding it — checking without attempting first is the single easiest way to fool yourself into thinking you've learned this.
- Structurally, what's actually different between a ConfigMap and a Secret?
- Why doesn't base64 encoding provide real security for Secret values?
- Name the three ways a Pod can consume a ConfigMap or Secret.
- If you update a ConfigMap that's mounted as a volume in a running Pod, does the Pod need to restart to see the new value? What about if it's consumed as an environment variable?
- Why did
pod-wrongns-demofail even though the ConfigMap it referenced definitely existed in the cluster? - What Secret type would you create to let a Pod pull an image from Azure Container Registry, and what object field references it?
Show answers
- Almost nothing structurally — a Secret's values are stored base64-encoded and it's conventionally treated (and restricted via RBAC) as sensitive; a ConfigMap stores plaintext and is treated as general configuration.
- Base64 is a reversible encoding, not encryption — anyone with read access to the Secret object can decode it in one command; it exists so arbitrary data survives YAML/JSON transport, not to hide the value.
- As environment variables (one key or all keys), as mounted files in a
volume, or referenced directly by another field (e.g.
imagePullSecrets). - Mounted-file consumption updates automatically (via the kubelet's periodic sync) without a Pod restart, though the app must notice the file changed; env-var consumption requires a Pod restart because env vars are only read at container start.
- ConfigMaps and Secrets are namespaced objects — a Pod can only reference one that exists in its own namespace, regardless of what exists elsewhere in the cluster.
- A
kubernetes.io/dockerconfigjsonSecret, created withkubectl create secret docker-registry ..., referenced viaspec.imagePullSecretsin the Pod spec.
Further reading & sources
- ConfigMaps - the concept page for storing non-sensitive configuration in the cluster.
- Secrets - what Secrets are, their types, and why base64 is encoding not encryption.
- Configure a Pod to Use a ConfigMap - the task guide for env-var and volume consumption.
- Pull an Image from a Private Registry - creating and using a docker-registry Secret, as you will for ACR.
- Encrypting Confidential Data at Rest - the cluster-admin control that gives Secrets real protection in production.
Next
06-storage-pv-and-pvc — configuration is externalized; next, give Pods durable storage that survives beyond a single Pod's lifetime.