Chapter 61: Kubernetes Storage in Depth: Volumes, PV, PVC, StorageClass, CSI and Snapshots
61.6 ConfigMap and Secret volumes, projected volumes and live config reloads
Why: the problem
Suppose MakeMyTrip runs an nginx front end for its flight search. The nginx.conf file, a list of feature flags and a TLS certificate must all be inside the container. Baking them into the image means a new image for every config change, and putting the private key in the image means anyone who pulls the image can read it. Kubernetes solves this with three volume types that come from API objects instead of disks. A configMap volume turns each key of a ConfigMap into a file. A secret volume does the same for a Secret, on a RAM-backed tmpfs so the key never touches the node's disk. A projected volume combines several sources (ConfigMaps, Secrets, the downward API and a service account token) into one folder. These volumes have one big advantage over environment variables: when you update the ConfigMap or Secret, kubelet updates the files inside running pods within about a minute. The app still has to notice the change (nginx needs a reload), and there is one trap: a subPath mount never gets updates. In this lesson we mount an nginx config from a ConfigMap and watch it update, mount a TLS Secret with safe file modes, build one projected folder with config, pod info and a short-lived token, and see why subPath and immutable objects behave differently.
What: every term in plain English
- configMap volume — Each key in a ConfigMap becomes a file in the mount folder.
- secret volume — Same for a Secret; files sit on tmpfs in RAM.
- items — Choose which keys to mount and under what file names.
- defaultMode — File permissions for mounted keys, for example 0440.
- projected volume — One folder built from several sources.
- downwardAPI — Exposes pod fields like name, namespace and labels as files.
- serviceAccountToken — A short-lived, audience-bound token file that kubelet refreshes.
- subPath — Mounts one file or subfolder; it does not receive updates.
- immutable: true — Marks a ConfigMap or Secret read-only forever; kubelet stops watching it.
- ..data symlink — How kubelet swaps all files at once during an update.
How: step by step
- Create a namespace and the ConfigMap with nginx.conf. Use kubectl create configmap --from-file.
- Mount it in an nginx pod and curl it. The config is live.
- Change the ConfigMap and wait. The file changes in the pod; reload nginx.
- Create a TLS Secret and mount it with mode 0400. Check the tmpfs mount.
- Build a projected volume. Config, labels and a token in one folder.
- Compare subPath and immutable. See which ones update.
- Clean up. Keep the cluster for 61.7.
Example 1: Serve nginx with a config file from a ConfigMap and update it live
Goal. Serve nginx with a config file from a ConfigMap and update it live.
default.conf
server {
listen 80;
location /health { return 200 "ok v1\n"; }
location / { return 200 "MakeMyTrip flights v1\n"; }
}
nginx-cm.yaml
apiVersion: v1
kind: Pod
metadata:
name: web
namespace: storage
labels:
app: web
spec:
volumes:
- name: conf
configMap:
name: nginx-conf
containers:
- name: nginx
image: nginx:1.27
volumeMounts:
- name: conf
mountPath: /etc/nginx/conf.d
readOnly: true
resources:
requests: {cpu: 20m, memory: 32Mi}
limits: {memory: 64Mi}
What each line does
--from-file— The file name default.conf becomes the key, and the key becomes the file.mountPath /etc/nginx/conf.d— Replaces the folder, so only our file is there.readOnly— Config should never be edited from inside the pod.Update delay— kubelet sync period plus cache, usually under 60-90 seconds.nginx -s reload— nginx reads files only at start or reload.
Green output
$ kubectl create configmap nginx-conf -n storage --from-file=default.conf
$ kubectl apply -f nginx-cm.yaml
$ kubectl exec -n storage web -- curl -s localhost/
MakeMyTrip flights v1
$ sed -i 's/v1/v2/g' default.conf
$ kubectl create configmap nginx-conf -n storage --from-file=default.conf --dry-run=client -o yaml | kubectl apply -f -
$ sleep 60; kubectl exec -n storage web -- cat /etc/nginx/conf.d/default.conf | grep v2
location / { return 200 "MakeMyTrip flights v2\n"; }
$ kubectl exec -n storage web -- nginx -s reload
$ kubectl exec -n storage web -- curl -s localhost/
MakeMyTrip flights v2
Expected result. The file inside the running pod changed without a restart. nginx served v2 only after the reload; many teams add a small sidecar that watches the folder and reloads.
Example 2: Mount a TLS Secret safely with defaultMode and items
Goal. Mount a TLS Secret safely with defaultMode and items.
tls-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: tls-check
namespace: storage
spec:
securityContext:
runAsUser: 101
fsGroup: 101
volumes:
- name: certs
secret:
secretName: flights-tls
defaultMode: 0440
items:
- key: tls.crt
path: server.crt
- key: tls.key
path: server.key
mode: 0400
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
volumeMounts:
- name: certs
mountPath: /etc/tls
readOnly: true
resources:
requests: {cpu: 10m, memory: 16Mi}
limits: {memory: 32Mi}
What each line does
openssl req— Makes a self-signed test cert for flights.example.com; never commit real keys.kubectl create secret tls— Stores tls.crt and tls.key keys.items— Renames keys to server.crt and server.key.mode: 0400— Only the owner can read the private key.fsGroup: 101— Files are group-owned by 101 so the non-root user can read them.
Green output
$ openssl req -x509 -nodes -newkey rsa:2048 -days 30 -subj "/CN=flights.example.com" -keyout tls.key -out tls.crt
$ kubectl create secret tls flights-tls -n storage --cert=tls.crt --key=tls.key
$ kubectl apply -f tls-pod.yaml
$ kubectl exec -n storage tls-check -- ls -lL /etc/tls
-r--r----- 1 root 101 1123 Oct 25 02:10 server.crt
-r-------- 1 root 101 1704 Oct 25 02:10 server.key
$ kubectl exec -n storage tls-check -- df -h /etc/tls
Filesystem Size Used Available Use% Mounted on
tmpfs 7.6G 12K 7.6G 0% /etc/tls
$ rm tls.key
Expected result. The key is readable only with the right mode, sits on tmpfs, and was deleted from the laptop after upload.
Example 3: Build one projected folder with config, pod labels and a short-lived token
Goal. Build one projected folder with config, pod labels and a short-lived token.
projected.yaml
apiVersion: v1
kind: Pod
metadata:
name: all-in-one
namespace: storage
labels:
app: search
team: flights
spec:
volumes:
- name: runtime
projected:
defaultMode: 0444
sources:
- configMap:
name: nginx-conf
items:
- key: default.conf
path: config/default.conf
- downwardAPI:
items:
- path: pod/labels
fieldRef:
fieldPath: metadata.labels
- path: pod/namespace
fieldRef:
fieldPath: metadata.namespace
- serviceAccountToken:
audience: flights-api
expirationSeconds: 3600
path: token/flights-api
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
volumeMounts:
- name: runtime
mountPath: /runtime
readOnly: true
resources:
requests: {cpu: 10m, memory: 16Mi}
limits: {memory: 32Mi}
What each line does
projected.sources— Three different sources in one mount.path with folders— config/, pod/ and token/ keep things tidy.downwardAPI labels— The app can read its own labels without calling the API.audience: flights-api— The token is only valid for that audience.expirationSeconds— kubelet refreshes the token before it expires.
Green output
$ kubectl apply -f projected.yaml
$ kubectl exec -n storage all-in-one -- find /runtime -type f -o -type l | grep -v '\.\.'
/runtime/config/default.conf
/runtime/pod/labels
/runtime/pod/namespace
/runtime/token/flights-api
$ kubectl exec -n storage all-in-one -- cat /runtime/pod/labels
app="search"
team="flights"
$ kubectl exec -n storage all-in-one -- sh -c 'cut -d. -f2 /runtime/token/flights-api | base64 -d 2>/dev/null' | head -c 120
{"aud":["flights-api"],"exp":1793336400,"iat":1793332800,"iss":"https://kubernetes.default.svc.cluster.local",
Expected result. One read-only folder now gives the app its config, its own identity, and a token that expires in an hour. The default kube-api-access volume in every pod is built the same way.
Every command, step by step (kind lab with kubectl)
-
Create the ConfigMap
kubectl create configmap nginx-conf -n storage --from-file=default.conf kubectl get cm nginx-conf -n storage -o yaml | head -8What it does: Key default.conf holds the file.
data: default.conf: | server { listen 80; -
Run nginx
kubectl apply -f nginx-cm.yaml kubectl exec -n storage web -- curl -s localhost/healthWhat it does: Our config is live.
ok v1 -
See the symlink trick
kubectl exec -n storage web -- ls -la /etc/nginx/conf.dWhat it does: Files point into ..data, which kubelet swaps in one step.
lrwxrwxrwx ..data -> ..2026_10_25_02_01_12.3417 lrwxrwxrwx default.conf -> ..data/default.conf -
Update and watch
sed -i 's/v1/v2/g' default.conf kubectl create configmap nginx-conf -n storage --from-file=default.conf --dry-run=client -o yaml | kubectl apply -f - kubectl exec -n storage web -- sh -c 'while ! grep -q v2 /etc/nginx/conf.d/default.conf; do sleep 5; done; echo updated'What it does: Usually within a minute.
updated -
Reload nginx
kubectl exec -n storage web -- nginx -s reload kubectl exec -n storage web -- curl -s localhost/What it does: Now v2 is served.
MakeMyTrip flights v2 -
Try subPath
kubectl run sub -n storage --image=busybox:1.36 --overrides='{"spec":{"volumes":[{"name":"c","configMap":{"name":"nginx-conf"}}],"containers":[{"name":"sub","image":"busybox:1.36","command":["sleep","3600"],"volumeMounts":[{"name":"c","mountPath":"/app/default.conf","subPath":"default.conf"}]}]}}' kubectl exec -n storage sub -- grep -c v2 /app/default.confWhat it does: Shows the file at the time the pod started.
2 -
Change again and check subPath
sed -i 's/v2/v3/g' default.conf kubectl create configmap nginx-conf -n storage --from-file=default.conf --dry-run=client -o yaml | kubectl apply -f - sleep 90; kubectl exec -n storage sub -- grep -c v3 /app/default.confWhat it does: subPath never updates.
0 command terminated with exit code 1 -
Create and mount the TLS Secret
openssl req -x509 -nodes -newkey rsa:2048 -days 30 -subj "/CN=flights.example.com" -keyout tls.key -out tls.crt kubectl create secret tls flights-tls -n storage --cert=tls.crt --key=tls.key kubectl apply -f tls-pod.yaml kubectl exec -n storage tls-check -- ls -lL /etc/tlsWhat it does: Modes 0440 and 0400.
-r--r----- server.crt -r-------- server.key -
Run the projected pod
kubectl apply -f projected.yaml kubectl exec -n storage all-in-one -- cat /runtime/pod/namespaceWhat it does: One folder, many sources.
storage -
Make a ConfigMap immutable
kubectl patch cm nginx-conf -n storage -p '{"immutable":true}' kubectl create configmap nginx-conf -n storage --from-file=default.conf --dry-run=client -o yaml | kubectl apply -f -What it does: Changes are now rejected; you must create a new name.
The ConfigMap "nginx-conf" is invalid: data: Forbidden: field is immutable when `immutable` is set -
Clean up
kubectl delete pod -n storage web tls-check all-in-one sub kubectl delete cm nginx-conf -n storage; kubectl delete secret flights-tls -n storage rm -f tls.key tls.crtWhat it does: Keep the cluster for 61.7.
pod "web" deleted
When it is red 1
MountVolume.SetUp failed for volume "conf" : configmap "nginx-conf" not found
Cause. The ConfigMap is missing or in another namespace.
Fix, step by step
- Create it in the pod's namespace first.
- Or set optional: true if the app can start without it.
When it is red 2
open() "/etc/tls/server.key" failed (13: Permission denied)
Cause. The non-root user cannot read the key with mode 0400 owned by root.
Fix, step by step
- Set fsGroup and use 0440, or runAsUser matching the owner.
When it is red 3
Config changed but the app still uses the old value
Cause. Env vars never update, subPath never updates, or the app did not reload.
Fix, step by step
- Use a volume without subPath.
- Reload the app, or roll the Deployment with a checksum annotation.
When it is red 4
data: Forbidden: field is immutable when `immutable` is set
Cause. The object is immutable.
Fix, step by step
- Create a new name like nginx-conf-v4 and point the pod to it.
Practice tasks
Easy Task 1: ConfigMap
Why: Mount.
What to do: Serve nginx from a ConfigMap.
- --from-file.
Solution
kubectl exec -n storage web -- curl -s localhost/Easy Task 2: Files
Why: Inspect.
What to do: List the ..data symlinks.
- ls -la.
Solution
kubectl exec -n storage web -- ls -la /etc/nginx/conf.dEasy Task 3: Secret
Why: Mount.
What to do: Mount the TLS Secret.
- items.
Solution
kubectl exec -n storage tls-check -- ls -lL /etc/tlsMedium Task 4: Update
Why: Live.
What to do: Update the ConfigMap and wait for the file.
- About 60 seconds.
Solution
kubectl exec -n storage web -- cat /etc/nginx/conf.d/default.confMedium Task 5: Reload
Why: App.
What to do: Reload nginx after the update.
- nginx -s reload.
Solution
kubectl exec -n storage web -- nginx -s reloadMedium Task 6: subPath
Why: Trap.
What to do: Show that subPath does not update.
- Compare two pods.
Solution
kubectl exec -n storage sub -- cat /app/default.confMedium Task 7: Projected
Why: Combine.
What to do: Run the all-in-one pod.
- find /runtime.
Solution
kubectl exec -n storage all-in-one -- find /runtimeMedium Task 8: Immutable
Why: Lock.
What to do: Make a ConfigMap immutable and try to change it.
- Forbidden.
Solution
kubectl patch cm nginx-conf -n storage -p '{"immutable":true}'Hard Task 9: Flights
Why: Main task.
What to do: Build the full Deployment with one projected volume.
- mode 0400 for the key.
Solution
kubectl get deploy -n storageHard Task 10: Explain
Why: Interview.
What to do: Explain env vars vs volume files for config.
- Updates.
Solution
kubectl explain pod.spec.volumes.projectedHomework: try at home
For MakeMyTrip flights, create a ConfigMap with nginx.conf and a flags.json, a TLS Secret, and a Deployment whose pods mount config and certs in one projected volume at /etc/app with the key at mode 0400. Change a flag and prove the file updated in the running pod without a restart.
Recap
- ConfigMap and Secret volumes turn keys into files.
- Volume files update in running pods; env vars and subPath do not.
- Secret files live on tmpfs; set tight modes like 0400.
- Projected volumes combine config, pod info and tokens in one folder.
- Next: expanding volumes, snapshots and backups.
FAQ
Q. Are Secrets encrypted?
Only base64 in the API by default. Turn on encryption at rest (EKS uses KMS envelope encryption) and limit RBAC.
Q. How big can a ConfigMap be?
1 MiB. Large files belong in a volume or object storage.
Q. How do I restart pods on config change?
Put a checksum of the config in a pod template annotation, as Helm charts do, or run kubectl rollout restart.
Q. Can I use AWS Secrets Manager instead?
Yes, with the Secrets Store CSI driver or External Secrets Operator.
Q. Why is the token in a projected volume better?
It expires and is bound to an audience, unlike old long-lived service account secrets.
Q. Can a ConfigMap be optional?
Yes, optional: true lets the pod start if it is missing.
Interview questions
-
ConfigMap as env vs as volume?
Model answer: Env is read once at start; volume files update live, except subPath.
-
How are Secrets mounted on the node?
Model answer: As files on a tmpfs, so they are not written to disk.
-
What is a projected volume?
Model answer: A single mount combining ConfigMaps, Secrets, downward API and service account tokens.
-
Why does subPath not update?
Model answer: It bind-mounts a single file at start instead of following the ..data symlink.
-
When would you use immutable ConfigMaps?
Model answer: For large fleets, to cut API server load and prevent accidental changes.
Quick quiz
Q1. Where do Secret volume files live on the node?
On tmpfs in RAM.
Q2. Does a subPath mount get updates?
No.
Q3. Which source gives pod labels as files?
downwardAPI.
Q4. How do you set file permissions for mounted keys?
defaultMode or per-item mode.
Q5. What does immutable: true do?
Blocks changes and stops kubelet watching it.
Samajla ka?