Kubernetes Persistent Volumes — PV, PVC, StorageClass, and Stateful Workloads
The storage problem in Kubernetes#
Pods are ephemeral. When a pod dies, its filesystem dies with it. For stateless apps this is fine. For databases, message queues, and anything that stores data, this is a dealbreaker.
Kubernetes solves this with persistent volumes — storage that outlives pods.
The three-layer storage model#
Kubernetes separates storage into three abstractions:
| Layer | What it is | Who manages it |
|---|---|---|
| PersistentVolume (PV) | A piece of storage in the cluster | Cluster admin or dynamic provisioner |
| PersistentVolumeClaim (PVC) | A request for storage by a pod | Application developer |
| StorageClass | A template for dynamically creating PVs | Cluster admin |
This separation lets developers request storage without knowing the underlying infrastructure.
PersistentVolume (PV)#
A PV is a cluster-level resource that represents a piece of storage. It can be backed by local disks, NFS, cloud block storage (EBS, GCE PD, Azure Disk), or any CSI-compatible driver.
PV specification#
apiVersion: v1
kind: PersistentVolume
metadata:
name: database-pv
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: fast-ssd
csi:
driver: ebs.csi.aws.com
volumeHandle: vol-0abc123def456
PV lifecycle phases#
- Available — not yet bound to a claim
- Bound — bound to a PVC
- Released — the PVC was deleted, but the PV has not been reclaimed
- Failed — automatic reclamation failed
PersistentVolumeClaim (PVC)#
A PVC is how pods request storage. You specify how much storage you need, what access mode, and optionally which StorageClass to use.
PVC specification#
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: database-storage
namespace: production
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
storageClassName: fast-ssd
Using a PVC in a pod#
apiVersion: v1
kind: Pod
metadata:
name: postgres
spec:
containers:
- name: postgres
image: postgres:16
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: db-storage
volumes:
- name: db-storage
persistentVolumeClaim:
claimName: database-storage
How PVC binding works#
- You create a PVC with requirements (size, access mode, StorageClass)
- Kubernetes finds a matching PV (or dynamically creates one)
- The PV is bound to the PVC (one-to-one relationship)
- The pod mounts the PVC and reads/writes data
- If no matching PV exists and no StorageClass can provision one, the PVC stays in Pending state
StorageClass#
A StorageClass defines how to dynamically provision PVs. Instead of pre-creating PVs, Kubernetes creates them on demand when a PVC is submitted.
StorageClass specification#
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
type: gp3
iops: "5000"
throughput: "250"
encrypted: "true"
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
Common StorageClass parameters by provider#
| Provider | Provisioner | Key parameters |
|---|---|---|
| AWS EBS | ebs.csi.aws.com | type (gp3, io2), iops, throughput, encrypted |
| GCE PD | pd.csi.storage.gke.io | type (pd-ssd, pd-balanced), replication-type |
| Azure Disk | disk.csi.azure.com | skuName (Premium_LRS, StandardSSD_LRS) |
| Local path | rancher.io/local-path | (none, uses local node storage) |
Volume binding modes#
- Immediate — PV is provisioned as soon as the PVC is created (default)
- WaitForFirstConsumer — PV is provisioned only when a pod using the PVC is scheduled (recommended for topology-aware storage)
WaitForFirstConsumer prevents the situation where a PV is created in availability zone A but the pod is scheduled in zone B.
Access modes#
Access modes define how a volume can be mounted across nodes.
| Mode | Short name | Description |
|---|---|---|
| ReadWriteOnce | RWO | Mounted read-write by a single node |
| ReadOnlyMany | ROX | Mounted read-only by many nodes |
| ReadWriteMany | RWX | Mounted read-write by many nodes |
| ReadWriteOncePod | RWOP | Mounted read-write by a single pod (K8s 1.27+) |
Which mode to use#
- Databases (Postgres, MySQL) —
ReadWriteOnce(single writer) - Shared config or static assets —
ReadOnlyMany - Shared file storage (uploads, media) —
ReadWriteMany(requires NFS or similar) - Strict single-writer guarantee —
ReadWriteOncePod
Not all storage backends support all modes. EBS only supports RWO. EFS supports RWX. Check your CSI driver documentation.
Reclaim policies#
When a PVC is deleted, the reclaim policy determines what happens to the PV and its data.
| Policy | Behavior | Use case |
|---|---|---|
| Retain | PV and data are kept, manual cleanup required | Production databases, critical data |
| Delete | PV and underlying storage are deleted | Dev/test environments, ephemeral workloads |
| Recycle | Data is deleted (rm -rf /volume/*), PV is reused | Deprecated, do not use |
Best practice#
Use Retain for production data. Use Delete for dev/test. Never rely on Recycle.
For Retain volumes, after the PVC is deleted:
- The PV moves to
Releasedstate - Back up the data if needed
- Delete the PV object to clean up
- Or remove the
claimRefto make it available again
CSI drivers#
The Container Storage Interface (CSI) is the standard for connecting Kubernetes to storage backends. Every major cloud and storage vendor provides a CSI driver.
Popular CSI drivers#
| Driver | Storage type | Features |
|---|---|---|
| EBS CSI | AWS block storage | Snapshots, encryption, gp3/io2 |
| EFS CSI | AWS shared filesystem | RWX support, POSIX compliance |
| GCE PD CSI | GCP block storage | Regional disks, snapshots |
| Azure Disk CSI | Azure block storage | Premium SSD, Ultra Disk |
| Longhorn | Distributed block storage | Replication, backup, self-hosted |
| Rook-Ceph | Distributed storage (block, file, object) | Self-hosted, highly configurable |
| OpenEBS | Container-native storage | Multiple engines, self-hosted |
Installing a CSI driver#
Most managed Kubernetes services (EKS, GKE, AKS) include CSI drivers as add-ons. For self-managed clusters, install via Helm:
helm repo add aws-ebs-csi-driver https://kubernetes-sigs.github.io/aws-ebs-csi-driver
helm install aws-ebs-csi-driver aws-ebs-csi-driver/aws-ebs-csi-driver \
--namespace kube-system
Stateful workloads with StatefulSets#
StatefulSets are designed for stateful applications. They provide stable network identities and persistent storage for each replica.
StatefulSet with volumeClaimTemplates#
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 3
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:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 50Gi
What StatefulSets guarantee#
- Stable identifiers —
postgres-0,postgres-1,postgres-2(not random suffixes) - Per-replica storage — each pod gets its own PVC (
data-postgres-0,data-postgres-1) - Ordered deployment — pods start in order (0, then 1, then 2)
- Ordered termination — pods stop in reverse order
- PVCs persist — scaling down does not delete PVCs (data is preserved)
Volume expansion#
Resize volumes without downtime (if the StorageClass allows it).
Requirements#
- StorageClass must have
allowVolumeExpansion: true - The CSI driver must support expansion
- You can only increase size, never decrease
How to expand#
kubectl patch pvc database-storage -p \
'{"spec":{"resources":{"requests":{"storage":"200Gi"}}}}'
Some drivers require the pod to be restarted for the filesystem to be resized. Others support online expansion.
Backup strategies#
Persistent data needs backup. Kubernetes does not handle this natively.
Volume snapshots#
Kubernetes supports VolumeSnapshots (CSI feature):
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: db-backup-2026-03-29
spec:
volumeSnapshotClassName: csi-aws-snapclass
source:
persistentVolumeClaimName: database-storage
Restore from a snapshot by referencing it in a new PVC:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: database-restored
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 100Gi
dataSource:
name: db-backup-2026-03-29
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
Application-level backups#
- Velero — backs up Kubernetes resources and persistent volumes together
- Kasten K10 — enterprise Kubernetes backup and disaster recovery
- Stash — backup for Kubernetes workloads using restic
- pg_dump / mysqldump — application-aware backups run as CronJobs
Backup best practices#
- Use VolumeSnapshots for block-level backups — fast, consistent, point-in-time
- Use application-level backups for logical backups — portable, inspectable
- Test restores regularly — a backup you have never restored is not a backup
- Automate with CronJobs — schedule snapshots and dumps on a recurring basis
- Store backups off-cluster — use S3, GCS, or another region for disaster recovery
Common pitfalls#
- PVC stuck in Pending — no matching PV or StorageClass, check
kubectl describe pvc - Pod stuck in ContainerCreating — volume not yet attached, check node affinity and zone
- Data loss on scale-down — StatefulSet PVCs are not deleted, but Deployment PVCs can be
- Wrong access mode — trying RWX on EBS (only supports RWO)
- No volume expansion — forgot
allowVolumeExpansion: trueon StorageClass - Zone mismatch — PV in zone A, pod scheduled in zone B (use
WaitForFirstConsumer)
Visualize storage architecture on Codelit#
On Codelit, generate any Kubernetes architecture with stateful workloads and see how PVs, PVCs, and StorageClasses connect to your pods. Explore storage topology, replication, and backup flows visually.
Article #433 in the Codelit engineering series. Explore our full library of system design, infrastructure, and architecture guides at codelit.io.
Explore the WhatsApp architecture interactively
Try it →Try these templates
Cloud File Storage Platform
Dropbox-like file storage with sync, sharing, versioning, and real-time collaboration.
8 componentsKubernetes Container Orchestration
K8s cluster with pod scheduling, service mesh, auto-scaling, and CI/CD deployment pipeline.
9 componentsDropbox Cloud Storage Platform
Cloud file storage and sync with real-time collaboration, versioning, sharing, and cross-device sync.
10 componentsContinue learning
Kubernetes Init Containers: Database Migrations, Config Loading, and Dependency Waiting
6 min readkubernetesKubernetes DaemonSet Guide — Logging, Monitoring, Node Agents, and Rolling Updates
7 min readkubernetesKubernetes HPA Deep Dive — CPU, Custom Metrics, Scaling Policies, and KEDA
7 min readGo deeper on infrastructure and delivery
As an Amazon Associate I earn from qualifying purchases. Codelit may receive a commission at no extra cost to you.
The DevOps Handbook
Gene Kim, Jez Humble · 2021
Case studies from Etsy, Amazon, and Target on rewiring deployment pipelines and org structure.
Kubernetes: Up and Running
Brendan Burns, Joe Beda · 2022
Builds from a single Pod to multi-cluster deployments with YAML you type, not diagrams you skim.
4.6 (118)#10 in Web ServicesObservability Engineering
Charity Majors, Liz Fong-Jones · 2026
2026 rewrite covering OpenTelemetry rollout, telemetry cost control, and tracing LLM calls.
#11 in Software Design ToolsFundamentals of Data Engineering
Joe Reis, Matt Housley · 2022
Covers the pipeline lifecycle from ingestion to serving, with trade-off tables for each stage.
4.7 (897)#3 in Data Mining