29

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

TermMeaning
VolumeStorage attached to a pod
PVPersistentVolume — a provisioned piece of storage
PVCPersistentVolumeClaim — a pod's request for storage
StorageClassTemplate for dynamic provisioning
Access modeRWO / ROX / RWX sharing semantics
Reclaim policyRetain / Delete on PVC removal
emptyDirPod-lifetime scratch volume
BoundA PVC matched to a PV
Static/dynamic provisioningPre-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: PersistentVolume YAML 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 emptyDir and 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