Kubernetes Persistent Volumes and Storage Classes
Understand PersistentVolumes, PersistentVolumeClaims, and StorageClasses to give your Kubernetes workloads reliable, portable storage.
What you'll learn
- ✓The relationship between PVs, PVCs, and StorageClasses
- ✓Static vs dynamic provisioning
- ✓Access modes and reclaim policies
- ✓Using StorageClasses for different performance tiers
- ✓Practical patterns for databases and stateful workloads
Prerequisites
None — this post is self-contained.
Containers are ephemeral by design. When a pod restarts, its filesystem resets. Databases, file uploads, and any workload that needs data to survive restarts requires persistent storage. Kubernetes models this with three resources: PersistentVolumes, PersistentVolumeClaims, and StorageClasses.
The Three Resources
PersistentVolume (PV) represents a piece of storage in the cluster. It could be a disk on a cloud provider, an NFS share, or a local SSD. A PV exists independently of any pod.
PersistentVolumeClaim (PVC) is a request for storage by a user. It specifies how much space is needed and what access mode is required. Kubernetes binds a PVC to a matching PV.
StorageClass defines how storage is dynamically provisioned. Instead of pre-creating PVs, you define a StorageClass and let Kubernetes create PVs on demand when PVCs are submitted.
The mental model: a PV is the actual disk, a PVC is a ticket requesting a disk, and a StorageClass is the vending machine that creates disks automatically.
Static Provisioning
In static provisioning, a cluster administrator creates PVs ahead of time:
apiVersion: v1
kind: PersistentVolume
metadata:
name: data-pv
spec:
capacity:
storage: 50Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /mnt/data
A developer then creates a PVC that matches:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
Kubernetes binds the PVC to the PV based on capacity, access mode, and optional label selectors. The pod mounts the PVC:
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: data
mountPath: /var/lib/app/data
volumes:
- name: data
persistentVolumeClaim:
claimName: data-pvc
Static provisioning works but does not scale. Every PVC requires a pre-created PV. Dynamic provisioning solves this.
Dynamic Provisioning with StorageClasses
A StorageClass tells Kubernetes how to create PVs automatically. On AWS EKS:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
type: gp3
iops: "5000"
throughput: "250"
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
On Google GKE:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: pd.csi.storage.gke.io
parameters:
type: pd-ssd
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
Now a PVC references the StorageClass, and Kubernetes provisions the disk automatically:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: db-data
spec:
storageClassName: fast-ssd
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
When this PVC is created, the CSI driver provisions a 100 GB SSD, creates a PV object, and binds it to the PVC. No administrator intervention needed.
Access Modes
Access modes define how many nodes can mount a volume simultaneously:
| Mode | Abbreviation | Description |
|---|---|---|
| ReadWriteOnce | RWO | One node can mount read-write |
| ReadOnlyMany | ROX | Many nodes can mount read-only |
| ReadWriteMany | RWX | Many nodes can mount read-write |
| ReadWriteOncePod | RWOP | One pod can mount read-write |
Most block storage (EBS, Persistent Disk) supports only ReadWriteOnce. For ReadWriteMany, you need a filesystem-based solution like NFS, EFS, or CephFS.
ReadWriteOncePod (Kubernetes 1.27+) is stricter than ReadWriteOnce. RWO allows multiple pods on the same node to mount the volume. RWOP ensures exactly one pod across the entire cluster has write access, which is safer for databases.
Reclaim Policies
When a PVC is deleted, the reclaim policy on the PV determines what happens to the underlying storage:
- Delete removes the PV and the backing storage (the actual cloud disk). This is the default for dynamically provisioned volumes.
- Retain keeps the PV and storage intact. An administrator must manually clean up or rebind the PV.
For production databases, use Retain to prevent accidental data loss:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: database-storage
provisioner: ebs.csi.aws.com
parameters:
type: gp3
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
Volume Binding Mode
volumeBindingMode controls when provisioning happens:
- Immediate provisions the disk as soon as the PVC is created, even if no pod uses it yet. The disk is created in a random availability zone.
- WaitForFirstConsumer delays provisioning until a pod that uses the PVC is scheduled. The disk is created in the same availability zone as the node.
Always use WaitForFirstConsumer in multi-zone clusters. Without it, the disk might be provisioned in zone A while the pod is scheduled in zone B, and the pod will be stuck in Pending.
Volume Expansion
If a StorageClass has allowVolumeExpansion: true, you can grow a PVC by editing its spec:
kubectl patch pvc db-data -p '{"spec":{"resources":{"requests":{"storage":"200Gi"}}}}'
Kubernetes expands the underlying disk and, for most filesystem types, resizes the filesystem online. Shrinking is not supported.
StatefulSets and Volume Claim Templates
StatefulSets pair naturally with persistent volumes. Each replica gets its own PVC through a volume claim template:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
replicas: 3
serviceName: postgres
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
storageClassName: fast-ssd
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
This creates PVCs named data-postgres-0, data-postgres-1, and data-postgres-2. Each pod gets its own dedicated disk. When scaling up, new PVCs are created automatically. When scaling down, PVCs are preserved so data is not lost if you scale back up.
Multiple Storage Tiers
A practical cluster offers multiple StorageClasses for different workloads:
# Standard tier for general workloads
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: ebs.csi.aws.com
parameters:
type: gp3
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
---
# High-performance tier for databases
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: high-iops
provisioner: ebs.csi.aws.com
parameters:
type: io2
iops: "10000"
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
Developers choose the appropriate tier in their PVC’s storageClassName field.
Practical Recommendations
Use dynamic provisioning with StorageClasses for all new workloads. Set volumeBindingMode: WaitForFirstConsumer to avoid zone mismatch issues. Use reclaimPolicy: Retain for any storage class used by databases or other critical stateful applications. Pair StatefulSets with volume claim templates for replicated stateful workloads. Enable allowVolumeExpansion so you can grow disks without recreating PVCs.
Related articles
- Kubernetes Kubernetes DaemonSets Explained with Practical Examples
Learn how DaemonSets run exactly one pod per node for logging, monitoring, and networking agents. Includes tolerations and update strategies.
- Kubernetes Kubernetes Network Policies: Pod-to-Pod Security Guide
Lock down pod communication with Kubernetes NetworkPolicies. Learn ingress and egress rules, label selectors, and namespace isolation.
- Kubernetes Kubernetes Resource Limits, Requests, and QoS Classes
Set CPU and memory requests and limits correctly. Understand QoS classes, OOMKilled errors, and CPU throttling in Kubernetes.
- Kubernetes Kubernetes Rolling Updates and Rollback Strategies
Master zero-downtime deployments with Kubernetes rolling updates, rollback commands, and deployment strategies like blue-green and canary.