Storage in Kubernetes (Volumes, PV, PVC, StorageClass)
Video: Day 29/40 — Kubernetes Volume Simplified | PV, PVC & Storage Class • https://www.youtube.com/watch?v=2NzYX8_lX_0 • Duration: ~28 min
Key terms
| Term | Meaning |
|---|---|
| Volume | Storage attached to a pod |
| PV | PersistentVolume — a provisioned piece of storage |
| PVC | PersistentVolumeClaim — a pod's request for storage |
| StorageClass | Template for dynamic provisioning |
| Access mode | RWO / ROX / RWX sharing semantics |
| Reclaim policy | Retain / Delete on PVC removal |
| emptyDir | Pod-lifetime scratch volume |
| Bound | A PVC matched to a PV |
| Static/dynamic provisioning | Pre-made vs on-demand PVs |
Problem & solution
A container's filesystem dies with it, and a pod can be rescheduled to another node at any time. Apps that need durable data require storage that is decoupled from the pod (and node) lifecycle.
Solution: Decouple storage with PersistentVolumeClaims that bind PersistentVolumes (static or dynamically provisioned via StorageClasses), so data outlives pods.
The analogy
To use a warehouse you do not build one yourself; you fill out a claim form for the size and terms you need, and the operator hands you a rented unit that matches. Kubernetes splits storage the same way: a pod files a PersistentVolumeClaim describing the capacity and access it wants, and that claim binds to a matching PersistentVolume, the real piece of storage, which the pod then mounts without ever provisioning it directly.
Where this fits in the cluster
The same cluster entities appear in every day's notes; the <== marks what this day touches.
Why pods need volumes
A container's filesystem dies with it, and a pod can be rescheduled anytime. Volumes decouple data from the container lifecycle.
emptyDir -> scratch space, lives with the POD (gone when pod dies)
hostPath -> a directory on the NODE (tied to that one node)
PV / PVC -> real, durable storage that outlives pods (and nodes)
Non-persistent: emptyDir
Good for cache / scratch / sharing files between containers in the same pod.
spec:
containers:
- name: redis
image: redis
volumeMounts:
- name: redis-storage
mountPath: /data/redis
volumes:
- name: redis-storage
emptyDir: {} # deleted when the pod is removed
The PV / PVC model
Separation of concerns:
The PVC is matched to a PV with enough capacity and compatible accessModes + storageClassName, then they bind 1:1.
Access modes
A volume's access mode declares how many nodes can mount it and whether they get read-write or read-only access.
RWO ReadWriteOnce -> mounted read-write by ONE node
ROX ReadOnlyMany -> read-only by MANY nodes
RWX ReadWriteMany -> read-write by MANY nodes (needs NFS-like backend)
Reclaim policy (what happens when the PVC is deleted)
The reclaim policy decides the fate of the PV and its data once the claim is removed.
Retain -> keep the PV + data (manual cleanup) — safest
Delete -> delete the PV and the underlying storage
Recycle -> deprecated
Sample PV (hostPath, for a demo)
The PV is the supply side: it declares capacity, access mode, and the
backing storage (here a node hostPath for simplicity).
apiVersion: v1
kind: PersistentVolume
metadata:
name: task-pv-volume
labels:
type: local
spec:
storageClassName: standard
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/home/ubuntu/day29-storage-k8s"
Sample PVC (the request)
The PVC is the demand side: the app asks for a size, access mode, and class, and Kubernetes binds it to a matching PV.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: task-pv-claim
spec:
storageClassName: standard
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Mi # binds to a PV with >= this capacity
Pod that mounts the PVC
The pod references the PVC by name in volumes, then mounts it into a container
path — it never touches the PV directly.
apiVersion: v1
kind: Pod
metadata:
name: task-pv-pod
spec:
volumes:
- name: task-pv-storage
persistentVolumeClaim:
claimName: task-pv-claim
containers:
- name: task-pv-container
image: nginx
ports:
- containerPort: 80
volumeMounts:
- mountPath: "/usr/share/nginx/html"
name: task-pv-storage
kubectl get pv,pvc
# STATUS Bound means the PVC found and claimed a PV
Static vs dynamic provisioning
PVs can be hand-created by an admin ahead of time, or auto-created on demand by a StorageClass when a PVC appears.
STATIC -> admin pre-creates PVs by hand (like the demo above)
DYNAMIC -> a StorageClass auto-creates a PV when a PVC is made
StorageClass = a template/provisioner for on-demand volumes:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast
provisioner: kubernetes.io/aws-ebs # or a CSI driver
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
A PVC that names a StorageClass gets its PV created automatically — no admin step. The cluster's default StorageClass is used when none is specified.
Scenario: many pods, one StorageClass, zero hand-made PVs
This is the picture from the video. A Storage Admin wires a StorageClass to a real backend once; after that, app teams just submit PVCs and the cluster creates a PV for each — you don't have to specify (or pre-create) the PersistentVolume at all.
Developer/User writes only the PVC (size + accessMode + storageClassName). No
kind: PersistentVolumeYAML is authored by hand.
Each pod owns its claim — the PVC YAML names a class, not a volume:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: redis-pvc # one of these per app (nginx-pvc, nodejs-pvc, ...)
spec:
storageClassName: standard # the class the admin created; "" disables dynamic
accessModes:
- ReadWriteOnce # RWO: this claim is mounted by one node at a time
resources:
requests:
storage: 10Gi # the provisioner carves a PV of (at least) this size
# the pod just references its claim by name — never the PV
spec:
volumes:
- name: data
persistentVolumeClaim:
claimName: redis-pvc
containers:
- name: redis
image: redis
volumeMounts:
- mountPath: /data
name: data
Division of labor: the admin owns the StorageClass (and which backend + reclaim policy it uses); developers own PVCs. Because the class provisions on demand, three pods means three PVCs and three auto-created PVs — no manual PV step for any of them.
kubectl get storageclass # the template/provisioner the admin set up
kubectl get pvc # one Bound claim per app (redis/nginx/nodejs)
kubectl get pv # PVs the provisioner created automatically
End-to-end example: data that survives a pod restart
A PVC binds a PV; the pod can die and be recreated, the data stays.
End-to-end flow
A PVC binds durable storage so data outlives the pod that writes it.
Key takeaways
emptyDir= pod-scoped scratch;hostPath= one node; PV/PVC = durable.- PV = supply, PVC = demand; they bind 1:1 on capacity + accessMode + class.
- Access modes RWO/ROX/RWX; reclaim Retain/Delete.
- Static = hand-made PVs; dynamic = StorageClass provisions on demand.
- With a StorageClass you write only the PVC — the PV is auto-created, so many pods (nginx/redis/nodejs) each just bring their own claim.
Checklist
- [ ] Used an
emptyDirand saw it vanish with the pod - [ ] Created a PV + PVC and confirmed STATUS
Bound - [ ] Mounted the PVC in a pod and wrote/read data
- [ ] Explained static vs dynamic provisioning and StorageClasses
- [ ] Created several PVCs against one StorageClass and saw PVs auto-provisioned