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 diagram below shows where this day's topic fits.

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

Kubernetes splits durable storage into two objects so the people who supply storage and the people who consume it never have to coordinate directly: a PersistentVolume (PV) is the actual piece of storage, and a PersistentVolumeClaim (PVC) is a pod's request for some. The pod names a PVC, and Kubernetes binds that claim to a matching PV. Separation of concerns:

Graph legend — the Postgres pod's path to durable storage:

Graph nodeMaps toWhat it does
Podthe postgres podConsumes storage by naming a PVC, never a PV
PVC, a request for storagepostgres-pvcAsks for size + accessMode + class
PV, the actual storagepostgres-pvThe provisioned volume that gets bound 1:1
disk, NFS or cloudthe backing medium (hostPath/EBS/NFS)The real bytes behind the PV

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). This one will back a real PostgreSQL database's data directory.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: postgres-pv
  labels:
    type: local
spec:
  storageClassName: standard
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/mnt/data/postgres"

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: postgres-pvc
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. Here a real postgres:16 container stores its database files on the claim.

apiVersion: v1
kind: Pod
metadata:
  name: postgres
spec:
  volumes:
    - name: postgres-storage
      persistentVolumeClaim:
        claimName: postgres-pvc
  containers:
    - name: postgres
      image: postgres:16
      env:
        - name: POSTGRES_PASSWORD
          value: "********"
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
      ports:
        - containerPort: 5432
      volumeMounts:
        - mountPath: "/var/lib/postgresql/data"
          name: postgres-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.

Graph legend — one StorageClass auto-provisions a PV per claim:

Graph nodeMaps toWhat it does
gce-pd / aws-ebs / azure-file / nfsthe provisioner backendThe real storage system the class drives
StorageClassa kind: StorageClass objectTemplate the admin wires once to a backend
Storage Adminthe cluster operatorOwns the class + reclaim policy/backend
PVC 10Gi RWOeach app's PersistentVolumeClaimThe only YAML developers write
nginx / redis / nodejs podthe consuming podsEach mounts its own claim; PVs are auto-created

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.

Graph legend — the Postgres claim keeps data across pod restarts:

Graph nodeMaps toWhat it does
Apply PVC: request 5Gipostgres-pvcRequests durable storage
Bind PVC to a PVstatic or dynamic provisioningMatches the claim to a real volume
Apply Pod that mounts the PVCthe postgres podReferences the claim by name
Scheduler places pod; kubelet attachesscheduler + kubeletAttaches and mounts the volume on the node
Container writes to /data on the PVPostgres writing to its data dirPersists DB files onto the PV
Pod deleted and recreateda reschedule/restartPod lifecycle ends, claim stays
Same PV re-mounted: data intactthe rebound PVCRecovers the same data on the new pod

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