Kubernetes Storage: PV, PVC, and StorageClass¶
The problem this layer solves¶
A Pod's container filesystem is exactly as ephemeral as a plain Docker container's — gone on Pod deletion, exactly like the writable-layer behavior in Docker Fundamentals. On top of that, a Pod can be rescheduled to a different node entirely after a restart — so persistent storage in Kubernetes can't just be "a folder on this machine"; it has to be a resource the cluster tracks independently of any specific node, that can be attached to whichever node ends up running the Pod that needs it.
Three objects, three jobs¶
- PersistentVolume (PV) — represents one actual piece of storage: a cloud block volume, an NFS export, a local disk. Cluster-scoped, not tied to any namespace — it's infrastructure, not application config.
- PersistentVolumeClaim (PVC) — a namespaced request for storage
("10Gi, ReadWriteOnce") that a Pod actually references in its
volumes:list. A PVC binds to a PV that satisfies its request — the Pod spec never mentions the PV directly, only the PVC, which is the layer of indirection that lets the same Pod spec work whether the underlying PV came from AWS EBS, GCP Persistent Disk, or a local test cluster. - StorageClass — the template that makes PVCs self-service instead of requiring an administrator to hand-create a matching PV for every single PVC. A PVC that references a StorageClass triggers dynamic provisioning: the StorageClass's provisioner automatically creates a brand-new PV (an actual new cloud disk, API-provisioned on the spot) sized to match the claim.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
type: gp3
reclaimPolicy: Delete
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: db-data
spec:
storageClassName: fast-ssd
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
This exact pattern is what the
StatefulSet's volumeClaimTemplates
generates automatically, once per replica (data-db-0, data-db-1,
data-db-2) — each is a PVC just like the one above, each triggering
its own independent dynamic provisioning through the same StorageClass.
Access modes: the gotcha that breaks multi-node sharing¶
- ReadWriteOnce (RWO) — the volume can be mounted read-write by one node at a time. This is what almost all cloud block storage (AWS EBS, GCP Persistent Disk) actually supports.
- ReadOnlyMany (ROX) — many nodes, read-only.
- ReadWriteMany (RWX) — many nodes, read-write simultaneously. Requires a genuinely networked filesystem underneath (NFS, AWS EFS, Azure Files) — a plain cloud block disk fundamentally cannot do this, because block storage is attached to one machine's block device layer at a time, not shared over a network protocol.
A Deployment with replicas: 3, all trying to mount the same RWO PVC,
scheduled across different nodes, will have at most one replica
actually start — the others sit stuck in ContainerCreating, unable to
attach a volume already exclusively attached elsewhere. This is one of
the most common storage-related scheduling failures, and the fix is
either genuinely needing RWX-capable storage (NFS/EFS-backed
StorageClass), or restructuring so each replica has its own PVC
(exactly what a StatefulSet's volumeClaimTemplates does, sidestepping
the shared-RWO problem by not sharing one PVC across replicas at all).
Reclaim policy: what happens when the PVC is deleted¶
reclaimPolicy: Delete (the common default for dynamically provisioned
storage) means deleting the PVC also deletes the underlying cloud
disk — the data is gone, not just detached. reclaimPolicy: Retain
keeps the underlying PV (and its data) around even after the PVC is
deleted, requiring manual cleanup or manual rebinding later — the safer
choice for anything where an accidental kubectl delete pvc shouldn't
be equivalent to an accidental DROP DATABASE.
Common pitfall¶
A PVC stuck in Pending almost always means one of two things:
either no StorageClass matches what the PVC asked for (a typo in
storageClassName, or no default StorageClass configured on the
cluster when the PVC didn't specify one at all), or — for statically
provisioned storage — no existing PV satisfies the requested size and
access mode. kubectl describe pvc <name> surfaces the specific
scheduling/binding error directly; guessing at network or application
causes for what's actually a storage-binding failure wastes time that
one command would have saved.