Module 1.4: Volumes for Developers
Complexity:
[MEDIUM]- Essential for stateful applicationsTime to Complete: 40-50 minutes
Prerequisites: Module 1.3 (Multi-Container Pods)
Learning Outcomes
Section titled “Learning Outcomes”After completing this module, you will be able to:
- Create pods with emptyDir, hostPath, and PersistentVolumeClaim volumes
- Configure volume mounts to share data between containers in the same pod
- Debug volume mount errors including permission issues and missing PVCs
- Explain the difference between ephemeral and persistent volumes and when to use each
Why This Module Matters
Section titled “Why This Module Matters”Containers are ephemeral—when they restart, all data is lost. For real applications, you need persistent storage: databases need durable data, applications need shared files, and containers need ways to exchange data.
The CKAD tests practical volume usage: mounting ConfigMaps, sharing data between containers, and using persistent storage. You won’t manage StorageClasses (that’s CKA), but you will use PersistentVolumeClaims.
The Desk Drawer Analogy
A container’s filesystem is like a whiteboard—useful while you’re there, but wiped when you leave. An
emptyDirvolume is like a shared table in a meeting room—everyone in the meeting can use it, but it’s cleared when the meeting ends. A PersistentVolume is like your desk drawer—it’s yours, persists between workdays, and contains your important files.
Volume Types for Developers
Section titled “Volume Types for Developers”Quick Reference
Section titled “Quick Reference”| Volume Type | Persistence | Sharing | Use Case |
|---|---|---|---|
emptyDir | Pod lifetime | Between containers | Scratch space, caches |
hostPath | Node lifetime | No | Node access (dev only) |
configMap | Cluster lifetime | Read-only | Configuration files |
secret | Cluster lifetime | Read-only | Sensitive data |
persistentVolumeClaim | Beyond pod | Depends | Databases, stateful apps |
projected | Varies | Read-only | Combine multiple sources |
emptyDir: Temporary Shared Storage
Section titled “emptyDir: Temporary Shared Storage”An emptyDir is created when a Pod starts and deleted when the Pod is removed. Perfect for:
- Sharing files between containers
- Scratch space for computation
- Caches
Basic emptyDir
Section titled “Basic emptyDir”apiVersion: v1kind: Podmetadata: name: emptydir-demospec: containers: - name: writer image: busybox command: ["sh", "-c", "echo 'Hello' > /data/message && sleep 3600"] volumeMounts: - name: shared mountPath: /data - name: reader image: busybox command: ["sh", "-c", "cat /data/message && sleep 3600"] volumeMounts: - name: shared mountPath: /data volumes: - name: shared emptyDir: {}Memory-Backed emptyDir
Section titled “Memory-Backed emptyDir”For high-speed scratch space:
volumes:- name: cache emptyDir: medium: Memory # Uses RAM instead of disk sizeLimit: 100Mi # Limit memory usageConfigMap Volumes
Section titled “ConfigMap Volumes”Mount ConfigMaps as files. Each key becomes a file.
Create ConfigMap
Section titled “Create ConfigMap”# From literalsk create configmap app-config \ --from-literal=log_level=debug \ --from-literal=api_url=http://api.example.com
# From filek create configmap nginx-config --from-file=nginx.confMount as Volume
Section titled “Mount as Volume”apiVersion: v1kind: Podmetadata: name: config-demospec: containers: - name: app image: busybox command: ["sh", "-c", "cat /config/log_level && sleep 3600"] volumeMounts: - name: config mountPath: /config volumes: - name: config configMap: name: app-configResult:
/config/├── log_level # Contains "debug"└── api_url # Contains "http://api.example.com"Pause and predict: When you mount a ConfigMap as a volume to
/etc/app, what happens to any existing files already at/etc/appinside the container image? What if you only want to add one file without wiping out the rest?
Mount Specific Keys
Section titled “Mount Specific Keys”volumes:- name: config configMap: name: app-config items: - key: log_level path: logging/level.txt # Custom pathSubPath: Mount Single File Without Overwriting
Section titled “SubPath: Mount Single File Without Overwriting”volumeMounts:- name: config mountPath: /etc/app/config.yaml # Specific file subPath: config.yaml # Key from ConfigMapSecret Volumes
Section titled “Secret Volumes”Like ConfigMaps but for sensitive data. Mounted files are tmpfs (memory-backed).
Create Secret
Section titled “Create Secret”k create secret generic db-creds \ --from-literal=username=admin \ --from-literal=password=secret123Mount Secret
Section titled “Mount Secret”apiVersion: v1kind: Podmetadata: name: secret-demospec: containers: - name: app image: busybox command: ["sh", "-c", "cat /secrets/password && sleep 3600"] volumeMounts: - name: db-secrets mountPath: /secrets readOnly: true volumes: - name: db-secrets secret: secretName: db-credsFile Permissions
Section titled “File Permissions”volumes:- name: db-secrets secret: secretName: db-creds defaultMode: 0400 # Read-only by ownerPersistentVolumeClaim (PVC)
Section titled “PersistentVolumeClaim (PVC)”For data that survives pod restarts. As a developer, you request storage with a PVC; the cluster provisions it.
Create PVC
Section titled “Create PVC”apiVersion: v1kind: PersistentVolumeClaimmetadata: name: data-pvcspec: accessModes: - ReadWriteOnce # RWO, ROX, RWX resources: requests: storage: 1Gi # storageClassName: fast # Optional: specific classStop and think: You’re designing a pod that writes user uploads to a volume. If the pod crashes and gets rescheduled to a different node, what happens to the uploaded files with
emptyDirvsPersistentVolumeClaim? This distinction is critical for the exam.
Access Modes
Section titled “Access Modes”| Mode | Short | Description |
|---|---|---|
ReadWriteOnce | RWO | One node can mount read-write |
ReadOnlyMany | ROX | Many nodes can mount read-only |
ReadWriteMany | RWX | Many nodes can mount read-write |
Use PVC in Pod
Section titled “Use PVC in Pod”apiVersion: v1kind: Podmetadata: name: pvc-demospec: containers: - name: app image: nginx volumeMounts: - name: data mountPath: /data volumes: - name: data persistentVolumeClaim: claimName: data-pvcImperative PVC Creation
Section titled “Imperative PVC Creation”# No direct imperative command, but quick YAMLcat << 'EOF' | k apply -f -apiVersion: v1kind: PersistentVolumeClaimmetadata: name: my-pvcspec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 1GiEOFProjected Volumes
Section titled “Projected Volumes”Combine multiple sources into one mount point.
apiVersion: v1kind: Podmetadata: name: projected-demospec: containers: - name: app image: busybox command: ["sh", "-c", "ls -la /projected && sleep 3600"] volumeMounts: - name: all-config mountPath: /projected volumes: - name: all-config projected: sources: - configMap: name: app-config - secret: name: app-secrets - downwardAPI: items: - path: "labels" fieldRef: fieldPath: metadata.labelsCommon Volume Patterns
Section titled “Common Volume Patterns”Pattern 1: Shared Scratch Space
Section titled “Pattern 1: Shared Scratch Space”spec: containers: - name: processor image: processor volumeMounts: - name: scratch mountPath: /tmp/work - name: uploader image: uploader volumeMounts: - name: scratch mountPath: /data volumes: - name: scratch emptyDir: {}Pattern 2: Config + Secrets
Section titled “Pattern 2: Config + Secrets”spec: containers: - name: app image: myapp volumeMounts: - name: config mountPath: /etc/app - name: secrets mountPath: /etc/secrets readOnly: true volumes: - name: config configMap: name: app-config - name: secrets secret: secretName: app-secretsPattern 3: Init Container Prepares Data
Section titled “Pattern 3: Init Container Prepares Data”spec: initContainers: - name: download image: curlimages/curl command: ["curl", "-o", "/data/app.tar", "http://example.com/app.tar"] volumeMounts: - name: app-data mountPath: /data containers: - name: app image: myapp volumeMounts: - name: app-data mountPath: /app volumes: - name: app-data emptyDir: {}What would happen if: You update a ConfigMap that’s mounted as a volume in a running pod using
subPath. Does the pod see the updated values? What about withoutsubPath? Understanding this difference can save you debugging time in the exam.
Troubleshooting Volumes
Section titled “Troubleshooting Volumes”Check Volume Status
Section titled “Check Volume Status”# Pod volumesk describe pod myapp | grep -A10 Volumes
# PVC statusk get pvc
# PVC detailsk describe pvc data-pvcCommon Issues
Section titled “Common Issues”| Symptom | Cause | Solution |
|---|---|---|
| Pod stuck Pending | PVC not bound | Check PV availability |
| Permission denied | Wrong mode/user | Set securityContext.fsGroup |
| File not found | Wrong mountPath | Verify paths match |
| ConfigMap not updating | Mounted files cached | Restart pod or use subPath carefully |
Fix Permission Issues
Section titled “Fix Permission Issues”spec: securityContext: fsGroup: 1000 # Group ID for volume files containers: - name: app image: myapp securityContext: runAsUser: 1000Did You Know?
Section titled “Did You Know?”-
ConfigMaps and Secrets are eventually consistent. When you update them, pods see changes within a minute—but NOT if you used
subPathmounting. SubPath mounts are snapshots that don’t auto-update. -
emptyDir uses node disk by default but can use RAM (
medium: Memory). RAM-backed volumes are faster but count against container memory limits. -
PVC deletion is blocked if a pod is using it. Delete the pod first, then the PVC. Set
persistentVolumeReclaimPolicy: Deleteto auto-delete underlying storage when PVC is removed.
Common Mistakes
Section titled “Common Mistakes”| Mistake | Why It Hurts | Solution |
|---|---|---|
Forgetting volumeMounts | Volume defined but not mounted | Add mount to container |
Wrong mountPath | Files appear in unexpected location | Double-check paths |
Using subPath for live updates | Updates won’t propagate | Avoid subPath or restart pod |
| PVC with wrong access mode | Multi-node apps fail | Use RWX for shared access |
| Missing volume definition | Pod fails to start | Define volume in spec.volumes |
-
A developer’s pod caches processed thumbnails in
/tmp/cache. Every time the pod restarts, the cache is lost and thumbnails must be regenerated, causing a 5-minute warmup period. They’re using anemptyDirvolume. IsemptyDirthe right choice here, or should they switch to a PVC?Answer
It depends on whether the cache needs to survive pod restarts. `emptyDir` is tied to the pod lifecycle -- data is lost when the pod is deleted or rescheduled. If the 5-minute warmup is unacceptable, switch to a `PersistentVolumeClaim` with `ReadWriteOnce` access mode. However, if the pod rarely restarts and the cache can be rebuilt, `emptyDir` is simpler and doesn't consume persistent storage. For a middle ground, use `emptyDir` with `medium: Memory` for faster cache performance during the pod's lifetime, accepting that restarts clear it. -
Your application needs its config file at
/etc/app/config.yaml, but mounting the ConfigMap at/etc/appwipes out other files already in that directory. How do you mount just the single config file without overwriting the directory contents?Answer
Use `subPath` in the volume mount: ```yaml volumeMounts: - name: config mountPath: /etc/app/config.yaml subPath: config.yaml ``` This mounts only the specific key as a single file, preserving all other files in `/etc/app`. However, be aware of the trade-off: `subPath` mounts don't receive automatic updates when the ConfigMap changes. If you need live config updates, mount the entire ConfigMap to a different directory (e.g., `/config`) and have your app read from there instead. -
You’re deploying a web application across 3 replicas that all need to read and write to the same shared file storage for user uploads. Your PVC uses
ReadWriteOnce. Users report that uploads sometimes disappear. What’s wrong?Answer
`ReadWriteOnce` (RWO) only allows a single node to mount the volume read-write. If your 3 replicas are on different nodes, only pods on one node can actually write. Pods on other nodes either fail to mount or mount read-only, causing lost uploads. Switch to `ReadWriteMany` (RWX) access mode, which requires a storage backend that supports it (NFS, EFS, Azure Files, etc.). Not all storage classes support RWX -- check with `kubectl get storageclass` and your cluster's documentation. -
A pod has both a
configMapvolume and asecretvolume mounted. After updating the Secret withkubectl edit secret, the pod still shows the old secret values. The ConfigMap volume in the same pod DOES auto-update. What explains this inconsistency?Answer
The Secret is likely mounted using `subPath`, while the ConfigMap is mounted as a full directory. `subPath` mounts are snapshots taken at pod start time and never auto-update -- this applies to both ConfigMaps and Secrets. Full directory mounts are eventually consistent and update within roughly 60 seconds. To fix, either remove the `subPath` mount, or restart the pod to pick up the new Secret values. In production, many teams use `kubectl rollout restart` to force pods to remount updated Secrets.
Hands-On Exercise
Section titled “Hands-On Exercise”Task: Create a complete application with multiple volume types.
Scenario: Build an app that:
- Uses ConfigMap for configuration
- Uses Secret for credentials
- Uses emptyDir for shared cache between containers
- Uses PVC for persistent data
apiVersion: v1kind: ConfigMapmetadata: name: app-settingsdata: config.json: | {"logLevel": "info", "cacheEnabled": true}---apiVersion: v1kind: Secretmetadata: name: app-credstype: OpaquestringData: api-key: super-secret-key---apiVersion: v1kind: PersistentVolumeClaimmetadata: name: app-dataspec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 100Mi---apiVersion: v1kind: Podmetadata: name: volumes-appspec: containers: - name: app image: nginx volumeMounts: - name: config mountPath: /etc/app - name: secrets mountPath: /etc/secrets readOnly: true - name: cache mountPath: /tmp/cache - name: data mountPath: /data - name: cache-warmer image: busybox command: ["sh", "-c", "while true; do echo 'Cache data' > /cache/warm; sleep 30; done"] volumeMounts: - name: cache mountPath: /cache volumes: - name: config configMap: name: app-settings - name: secrets secret: secretName: app-creds - name: cache emptyDir: {} - name: data persistentVolumeClaim: claimName: app-dataVerification:
# Apply all resourcesk apply -f volumes-app.yaml
# Check pod runningk get pod volumes-app
# Verify mountsk exec volumes-app -c app -- ls -la /etc/appk exec volumes-app -c app -- ls -la /etc/secretsk exec volumes-app -c app -- ls -la /tmp/cachek exec volumes-app -c app -- ls -la /data
# Check PVC boundk get pvc app-data
# Cleanupk delete pod volumes-appk delete pvc app-datak delete configmap app-settingsk delete secret app-credsPractice Drills
Section titled “Practice Drills”Drill 1: emptyDir Sharing (Target: 3 minutes)
Section titled “Drill 1: emptyDir Sharing (Target: 3 minutes)”# Create pod with shared emptyDircat << 'EOF' | k apply -f -apiVersion: v1kind: Podmetadata: name: shared-podspec: containers: - name: writer image: busybox command: ["sh", "-c", "echo hello > /shared/msg && sleep 3600"] volumeMounts: - name: shared mountPath: /shared - name: reader image: busybox command: ["sh", "-c", "sleep 5 && cat /shared/msg && sleep 3600"] volumeMounts: - name: shared mountPath: /shared volumes: - name: shared emptyDir: {}EOF
# Verify sharing worksk logs shared-pod -c reader
# Cleanupk delete pod shared-podDrill 2: ConfigMap Volume (Target: 3 minutes)
Section titled “Drill 2: ConfigMap Volume (Target: 3 minutes)”# Create ConfigMapk create configmap web-config --from-literal=index.html="Welcome to CKAD!"
# Create pod using ConfigMapcat << 'EOF' | k apply -f -apiVersion: v1kind: Podmetadata: name: webspec: containers: - name: nginx image: nginx volumeMounts: - name: html mountPath: /usr/share/nginx/html volumes: - name: html configMap: name: web-configEOF
# Verify contentk exec web -- cat /usr/share/nginx/html/index.html
# Cleanupk delete pod webk delete cm web-configDrill 3: Secret Volume (Target: 3 minutes)
Section titled “Drill 3: Secret Volume (Target: 3 minutes)”# Create Secretk create secret generic db-pass --from-literal=password=mysecret
# Mount in podcat << 'EOF' | k apply -f -apiVersion: v1kind: Podmetadata: name: secret-podspec: containers: - name: app image: busybox command: ["sh", "-c", "cat /secrets/password && sleep 3600"] volumeMounts: - name: creds mountPath: /secrets readOnly: true volumes: - name: creds secret: secretName: db-passEOF
# Verify secret mountedk logs secret-pod
# Cleanupk delete pod secret-podk delete secret db-passDrill 4: PVC Usage (Target: 4 minutes)
Section titled “Drill 4: PVC Usage (Target: 4 minutes)”# Create PVCcat << 'EOF' | k apply -f -apiVersion: v1kind: PersistentVolumeClaimmetadata: name: test-pvcspec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 50MiEOF
# Use in podcat << 'EOF' | k apply -f -apiVersion: v1kind: Podmetadata: name: pvc-podspec: containers: - name: app image: nginx volumeMounts: - name: storage mountPath: /data volumes: - name: storage persistentVolumeClaim: claimName: test-pvcEOF
# Check PVC boundk get pvc test-pvc
# Write datak exec pvc-pod -- sh -c "echo 'Persistent!' > /data/test.txt"k exec pvc-pod -- cat /data/test.txt
# Cleanupk delete pod pvc-podk delete pvc test-pvcDrill 5: Projected Volume (Target: 4 minutes)
Section titled “Drill 5: Projected Volume (Target: 4 minutes)”# Create sourcesk create cm proj-config --from-literal=config=valuek create secret generic proj-secret --from-literal=secret=hidden
# Create pod with projected volumecat << 'EOF' | k apply -f -apiVersion: v1kind: Podmetadata: name: proj-pod labels: app: projectedspec: containers: - name: app image: busybox command: ["sh", "-c", "ls -la /projected && sleep 3600"] volumeMounts: - name: combined mountPath: /projected volumes: - name: combined projected: sources: - configMap: name: proj-config - secret: name: proj-secretEOF
# Check combined filesk exec proj-pod -- ls /projected
# Cleanupk delete pod proj-podk delete cm proj-configk delete secret proj-secretDrill 6: Complete Volume Challenge (Target: 6 minutes)
Section titled “Drill 6: Complete Volume Challenge (Target: 6 minutes)”Build from memory—no hints:
Create a pod data-processor that:
- Init container downloads “data” (simulate with echo)
- Main container processes data (nginx)
- Sidecar logs processing status
- Uses emptyDir for shared data
- Mounts a ConfigMap with processing settings
Solution
# Create ConfigMapk create cm processing-config --from-literal=mode=fast
# Create podcat << 'EOF' | k apply -f -apiVersion: v1kind: Podmetadata: name: data-processorspec: initContainers: - name: downloader image: busybox command: ["sh", "-c", "echo 'Downloaded data' > /data/input.txt"] volumeMounts: - name: data mountPath: /data containers: - name: processor image: nginx volumeMounts: - name: data mountPath: /data - name: config mountPath: /etc/config - name: logger image: busybox command: ["sh", "-c", "while true; do echo Processing $(cat /data/input.txt); sleep 5; done"] volumeMounts: - name: data mountPath: /data volumes: - name: data emptyDir: {} - name: config configMap: name: processing-configEOF
# Verifyk get pod data-processork logs data-processor -c loggerk exec data-processor -c processor -- cat /etc/config/mode
# Cleanupk delete pod data-processork delete cm processing-configNext Module
Section titled “Next Module”Part 1 Cumulative Quiz - Test your Application Design and Build knowledge.