Kubernetes Persistent Volumes (PVs) are a foundational concept for stateful applications, but they’re often misunderstood as just "storage." The truly surprising part is how they decouple storage provisioning from storage consumption, creating a flexible, dynamic system that works with nearly any backend.

Let’s see this in action. Imagine you have a PostgreSQL database pod that needs persistent storage.

First, a PersistentVolume (PV) object is created. This object represents a piece of storage in your cluster, abstracting away the underlying hardware. It could be an AWS EBS volume, an NFS share, or even local disk.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-postgres-data
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  nfs:
    server: 10.0.0.10
    path: /srv/nfs/postgres-data

Notice storageClassName: manual. This is a label for the PV. It doesn’t provision anything; it’s just a way to identify this specific volume.

Next, a PersistentVolumeClaim (PVC) is created by an application (or its developer) that needs storage. This is the consumer’s request.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-postgres-data
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: manual

Here, pvc-postgres-data requests 5Gi of storage with ReadWriteOnce access. Crucially, it also specifies storageClassName: manual. This is the binding mechanism. Kubernetes looks for a PV with a matching storageClassName and sufficient capacity. In our case, pv-postgres-data fits perfectly. Kubernetes then "binds" the PVC to the PV.

Finally, your application pod references the PVC.

apiVersion: v1
kind: Pod
metadata:
  name: postgres-pod
spec:
  containers:
  - name: postgres
    image: postgres:13
    ports:
    - containerPort: 5432
    volumeMounts:
    - name: postgres-storage
      mountPath: /var/lib/postgresql/data
  volumes:
  - name: postgres-storage
    persistentVolumeClaim:
      claimName: pvc-postgres-data

When the postgres-pod starts, Kubernetes mounts the storage represented by pv-postgres-data (because it’s bound to pvc-postgres-data) to /var/lib/postgresql/data inside the container. The database can now read and write its data, and that data will persist even if the pod is deleted and recreated.

The real magic is in the separation. The application developer only needs to know about PVCs and their requirements (size, access mode). The cluster administrator (or an automated provisioner) manages the actual PVs.

This decoupling allows for dynamic provisioning, too. If you omit storageClassName from the PVC (or set it to ""), and have a StorageClass defined in your cluster, Kubernetes will automatically provision a PV for you.

Here’s a StorageClass that uses AWS EBS:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: aws-ebs-gp2
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp2
  fsType: ext4

If a PVC requests this StorageClass (storageClassName: aws-ebs-gp2) and no matching PV exists, the kubernetes.io/aws-ebs provisioner kicks in, creates an EBS volume, and then creates a PV object for it, binding it to the PVC. The application gets its storage without anyone manually creating a PersistentVolume object.

The accessModes are critical: ReadWriteOnce (RWO) means the volume can be mounted as read-write by a single node. ReadOnlyMany (ROX) allows multiple nodes to mount it read-only. ReadWriteMany (RWX) allows multiple nodes to mount it read-write – this is typically for network-attached storage like NFS or Ceph. The underlying storage technology must support the requested accessMode. An AWS EBS volume, for instance, can only ever be RWO.

The persistentVolumeReclaimPolicy dictates what happens to the PV when the PVC is deleted. Retain means the PV and its data are kept, requiring manual cleanup. Delete means the underlying storage is also deleted. Recycle (deprecated) would scrub the data.

What most people don’t realize is that the StorageClass acts as a template for provisioning and also a filter for binding. When a PVC specifies a StorageClass, Kubernetes only considers PVs that either have the same storageClassName or have no storageClassName at all (if the PVC’s storageClassName is explicitly set to "" or omitted and the defaultStorageClass feature is enabled). This prevents a PVC intended for dynamic AWS EBS provisioning from accidentally binding to a manually created NFS PV.

Once you’ve mastered PVs and PVCs, the next logical step is understanding StatefulSets, which leverage persistent storage for ordered, stable deployment of stateful applications.

Want structured learning?

Take the full Storage Systems course →