Volumes & Persistent Storage — Full Reference¶
Reference: https://kubernetes.io/docs/concepts/storage/persistent-volumes/
Exam Priority — Quickest Path¶
For simple volume tasks, dry-run + minimal edit:
kubectl run alpine-pod --image=alpine:latest --restart=Never --dry-run=client -o yaml > pod.yaml
# edit pod.yaml — add volumeMounts + volumes sections only
kubectl apply -f pod.yaml
kubectl run cannot set volumes imperatively — you always need YAML for this. But you never start from scratch — dry-run gives you the scaffold, you only add the two sections you need.
When you need a PV/PVC: the question asks for persistent storage that survives pod deletion. A ConfigMap volume is read-only config — it's not persistent storage. Know which one the question is asking for.
Volume Types — What Each One Is¶
| Type | Survives pod deletion? | Use case |
|---|---|---|
emptyDir |
No — deleted with pod | Scratch space, sharing data between containers in the same pod |
configMap |
N/A — read from ConfigMap object | Inject config files into a container |
secret |
N/A — read from Secret object | Inject sensitive files into a container |
hostPath |
Yes — on the node | Access node filesystem (logs, Docker socket). Dangerous in prod. |
persistentVolumeClaim |
Yes — independent lifecycle | Databases, stateful apps, anything needing durable storage |
ConfigMap as a Volume¶
The raw notes exercise: mount a ConfigMap as a volume so a container can read it as files.
apiVersion: v1
kind: Pod
metadata:
name: alpine-pod
spec:
restartPolicy: Never
containers:
- name: alpine-container
image: alpine:latest
command: ["/bin/sh"]
args: ["-c", "tail -f /config/log.txt"]
volumeMounts:
- name: config-volume # must match the name in volumes below
mountPath: /config # directory inside the container where files appear
volumes:
- name: config-volume # the link between volumeMounts and this definition
configMap:
name: log-configmap # name of the ConfigMap object in the cluster
How the linking works: volumeMounts.name and volumes.name must be identical — that's how Kubernetes knows which volume to mount at which path. The name is arbitrary, just has to match.
Result: every key in log-configmap becomes a file inside /config/. If the ConfigMap has a key log.txt, the file /config/log.txt is created with its value as contents.
command vs args and the -c flag¶
command = overrides Docker's ENTRYPOINT. Here it's /bin/sh — the shell binary.
args = passed to the command as arguments. -c is a shell flag meaning "execute this string as a command."
Without -c: /bin/sh opens an interactive shell and waits for input. Nothing runs.
With -c "tail -f /config/log.txt": the shell runs that string as a command and starts tailing the file.
So the full execution is: /bin/sh -c "tail -f /config/log.txt" — run the shell, pass it a command string.
The $do alias¶
Set this at the start of every exam/session. Then:
Instead of typing --dry-run=client -o yaml every time. $do expands to that string.
--dry-run=client = generate the YAML locally, don't send it to the API server (nothing gets created).
-o yaml = output as YAML.
Together: generate a base pod spec you can edit before creating.
kubectl explain — When You Forget the Field Names¶
kubectl explain pod.spec.volumes # all volume types and their fields
kubectl explain pod.spec.containers.volumeMounts # what goes in volumeMounts
kubectl explain pod.spec.volumes.configMap # configMap volume fields specifically
kubectl explain pod.spec.volumes.persistentVolumeClaim
In the exam, if you forget whether it's mountPath or path, just run kubectl explain. It shows field names, types, and descriptions inline — faster than searching the docs.
Persistent Volumes — The Full Model¶
The problem emptyDir doesn't solve: a pod dies and restarts — emptyDir is wiped. A database needs storage that outlives the pod.
The PV/PVC model separates two concerns:
- PersistentVolume (PV) — the actual storage resource. Created by an admin (or dynamically by a StorageClass). Exists at the cluster level, independent of any pod.
- PersistentVolumeClaim (PVC) — a request for storage by a user/app. Says "I need 5Gi, ReadWriteOnce." Kubernetes binds it to a PV that satisfies the request.
- Pod — mounts the PVC. Doesn't care about the underlying storage type.
Admin creates PV (10Gi, hostPath)
↓
User creates PVC (requests 5Gi, RWO)
↓
Kubernetes binds PVC to PV (PV must satisfy PVC's requirements)
↓
Pod references PVC by name in volumes section
PersistentVolume YAML¶
apiVersion: v1
kind: PersistentVolume
metadata:
name: my-pv
spec:
capacity:
storage: 10Gi # how much storage this PV provides
accessModes:
- ReadWriteOnce # who can mount it and how (see below)
persistentVolumeReclaimPolicy: Retain # what happens when PVC is deleted (see below)
storageClassName: manual # must match the PVC's storageClassName
hostPath: # the actual storage backend
path: /mnt/data # path on the node (exam uses this — simple)
PersistentVolumeClaim YAML¶
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
namespace: default
spec:
accessModes:
- ReadWriteOnce # must match the PV
resources:
requests:
storage: 5Gi # how much you're requesting (PV must have >= this)
storageClassName: manual # must match the PV's storageClassName
Kubernetes binds the PVC to a PV when:
- The PV's storageClassName matches the PVC's storageClassName
- The PV's accessModes contains the mode the PVC requests
- The PV's capacity.storage >= PVC's resources.requests.storage
- The PV isn't already bound to another PVC
Access Modes¶
| Mode | Short | Meaning |
|---|---|---|
ReadWriteOnce |
RWO | Mounted read-write by one node at a time |
ReadOnlyMany |
ROX | Mounted read-only by many nodes simultaneously |
ReadWriteMany |
RWX | Mounted read-write by many nodes simultaneously |
ReadWriteOncePod |
RWOP | Mounted read-write by one pod (stricter than RWO) |
Critical: RWO means one node, not one pod. Multiple pods on the same node can all mount an RWO volume. RWX is what you need for shared storage across nodes (NFS, CephFS, etc.). hostPath only supports RWO.
Reclaim Policy¶
What happens to the PV when the PVC that's bound to it is deleted:
| Policy | What happens |
|---|---|
Retain |
PV stays, data stays. Status becomes Released. Admin must manually clean up. |
Delete |
PV and its underlying storage are deleted automatically (used with cloud storage) |
Recycle |
Deprecated. Don't use. |
For the exam: Retain = data is safe but PV is not reusable until manually cleaned. Delete = cloud-style cleanup.
Mount a PVC in a Pod¶
apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
image: nginx
volumeMounts:
- name: storage # must match volumes.name below
mountPath: /data # where to mount inside the container
volumes:
- name: storage
persistentVolumeClaim:
claimName: my-pvc # name of the PVC object
The pod doesn't reference the PV directly — it always goes through the PVC. The PVC abstracts away what storage is actually backing it.
StorageClass — Dynamic Provisioning¶
With static provisioning (above), an admin creates PVs manually before users can claim them. With dynamic provisioning, a StorageClass automatically creates a PV when a PVC is submitted.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast
provisioner: kubernetes.io/aws-ebs # what creates the actual storage
parameters:
type: gp2
reclaimPolicy: Delete
allowVolumeExpansion: true
PVC that uses it:
Kubernetes calls the provisioner → provisioner creates an EBS volume → PV is auto-created → PVC binds to it.
In the exam: if the question doesn't mention StorageClass and uses hostPath, you're doing static provisioning. If it mentions a StorageClass name, include storageClassName in both PV and PVC.
PV / PVC Lifecycle States¶
| State | Meaning |
|---|---|
Available |
PV exists, not yet bound to any PVC |
Bound |
PV is bound to a PVC |
Released |
PVC was deleted, PV still exists (Retain policy) — not yet available for new claims |
Failed |
Automatic reclamation failed |
kubectl get pv # check STATUS column
kubectl get pvc # check STATUS — should be Bound for a working setup
If PVC is stuck in Pending: the PV doesn't match (wrong storageClassName, wrong accessMode, insufficient capacity, or already bound).
All Volume Commands¶
# PersistentVolumes (cluster-scoped — no -n flag)
kubectl get pv
kubectl describe pv my-pv
kubectl delete pv my-pv
# PersistentVolumeClaims (namespace-scoped)
kubectl get pvc
kubectl get pvc -n my-namespace
kubectl describe pvc my-pvc
kubectl delete pvc my-pvc
# StorageClasses (cluster-scoped)
kubectl get storageclass
kubectl get sc # short name
# Check what's mounted in a pod
kubectl describe pod my-pod # Volumes section shows what's mounted
# kubectl explain for field reference
kubectl explain pv.spec
kubectl explain pvc.spec
kubectl explain pod.spec.volumes.persistentVolumeClaim
kubectl explain pod.spec.containers.volumeMounts
Common Exam Patterns¶
"Create a PV and PVC, mount it in a pod":
# 1. Create PV
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolume
metadata:
name: task-pv
spec:
capacity:
storage: 1Gi
accessModes: [ReadWriteOnce]
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
hostPath:
path: /mnt/data
EOF
# 2. Create PVC
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: task-pvc
spec:
accessModes: [ReadWriteOnce]
storageClassName: manual
resources:
requests:
storage: 500Mi
EOF
# 3. Verify binding
kubectl get pv,pvc # both should show Bound
# 4. Mount in pod — generate scaffold + edit
kubectl run task-pod --image=nginx --restart=Never --dry-run=client -o yaml > pod.yaml
# add volumeMounts + volumes sections, then:
kubectl apply -f pod.yaml
Debugging why PVC is Pending:
kubectl describe pvc my-pvc # Events section shows exact reason
kubectl get pv # check if a matching PV exists and is Available
Most common causes:
- storageClassName mismatch between PV and PVC
- PV accessModes doesn't include what PVC requests
- PV capacity is less than PVC's request
- PV is already Bound to a different PVC