Persistent Volumes (PVs) and Persistent Volume Claims (PVCs) in Kubernetes are how applications get stateful storage, but the real magic is how they abstract away the underlying storage infrastructure, letting you treat storage like any other resource.
Let’s see this in action. Imagine we have a simple Nginx deployment that needs to store its configuration files persistently.
First, we define a PersistentVolume (PV) that represents an actual piece of storage. This could be an NFS share, an AWS EBS volume, or even a local directory on a node.
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteMany
nfs:
path: /exports/nfs-data
server: 192.168.1.100
This PV declares that 5Gi of storage is available, can be mounted by multiple nodes simultaneously (ReadWriteMany), and is accessible via NFS from 192.168.1.100 at /exports/nfs-data.
Next, our application needs to request storage. This is done with a PersistentVolumeClaim (PVC).
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 3Gi
This PVC requests 3Gi of storage with the same ReadWriteMany access mode.
Now, Kubernetes’ control plane (specifically the pv-controller and persistent-volume-binder) looks for a PV that can satisfy this PVC. If it finds a match (like our nfs-pv which has enough capacity and compatible access modes), it binds them. The PVC will then show a Bound status, and the PV will show Bound as well, referencing the PVC.
Finally, we mount this bound PVC into our application’s Pod.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: nginx-config-volume
mountPath: /etc/nginx/conf.d
volumes:
- name: nginx-config-volume
persistentVolumeClaim:
claimName: nfs-pvc
When the nginx-deployment Pod starts, Kubernetes ensures that the storage represented by nfs-pvc (which is bound to nfs-pv) is mounted at /etc/nginx/conf.d inside the container. Any data written here will persist even if the Pod is deleted and recreated, as it resides on the external NFS storage.
The core problem PVs solve is decoupling the application’s storage requirements (PVCs) from the underlying storage provisioning (PVs). This means developers can request storage without knowing or caring if it’s NFS, iSCSI, cloud provider block storage, or even a local disk. The cluster administrator, on the other hand, provisions the actual storage and makes it available as PVs.
Behind the scenes, the persistent-volume-binder is the crucial component. It watches for new PVCs and available PVs. It uses a matching algorithm:
- Access Modes: The PVC’s
accessModesmust be compatible with the PV’saccessModes. For example, aReadWriteOncePVC can bind to aReadWriteOnceorReadWriteManyPV, but aReadWriteManyPVC can only bind to aReadWriteManyPV. - Capacity: The PV’s
capacity.storagemust be greater than or equal to the PVC’sresources.requests.storage. - Storage Class (if specified): If the PVC has a
storageClassName, it will only consider PVs with a matchingstorageClassNameor PVs that are dynamically provisioned by a StorageClass. If the PVC does not specify astorageClassName, it will only consider PVs that also do not specify astorageClassName(static provisioning).
This dynamic provisioning aspect is handled by StorageClass objects. A StorageClass defines a "class" of storage and includes a provisioner field. When a PVC specifies a storageClassName, Kubernetes tells the specified provisioner (e.g., kubernetes.io/aws-ebs, kubernetes.io/azure-disk, nfs.csi.k8s.io/) to create a new volume on demand. This volume is then automatically created as a PV and bound to the PVC. This is how you get on-demand storage creation without pre-provisioning every single volume.
A common point of confusion arises when accessModes seem compatible but binding fails. For instance, a PV might be ReadWriteMany and a PVC ReadWriteOnce. Kubernetes allows this binding because ReadWriteOnce is a subset of ReadWriteMany. However, the actual underlying storage technology might impose its own limitations. If you try to mount a ReadWriteOnce volume to more than one node, you’ll hit errors at the node level (e.g., "device or resource busy" or file system corruption), not at the Kubernetes binding level. The PV/PVC binding layer abstracts, but doesn’t magically override, the physical storage capabilities.
If you’re troubleshooting binding issues, check the events for both the PV and PVC (kubectl describe pv <pv-name> and kubectl describe pvc <pvc-name>). Look for messages indicating why a match couldn’t be made. Often, it’s a mismatch in accessModes or insufficient capacity, but it can also be due to misconfigured storageClassName or the absence of a suitable PV for static provisioning.
The next challenge you’ll encounter is managing the lifecycle of these dynamically provisioned volumes, especially when PVCs are deleted.