Kubernetes Storage (PV/PVC) Cheat Sheet
How PersistentVolumes, PersistentVolumeClaims, and StorageClasses provide durable, dynamically provisioned storage to pods.
Core Storage Objects
The three objects that make up Kubernetes' storage abstraction.
- PersistentVolume (PV)- Cluster-level storage resource provisioned by an admin or dynamically by a StorageClass
- PersistentVolumeClaim (PVC)- Namespaced request for storage by a pod; binds to a matching PV
- StorageClass- Defines a provisioner and parameters for dynamic PV provisioning, e.g. EBS gp3
- Access Modes- ReadWriteOnce, ReadOnlyMany, ReadWriteMany, ReadWriteOncePod - what the volume permits
StorageClass Definition
Dynamic provisioning class backed by AWS EBS gp3 volumes.
apiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: fast-ssdprovisioner: ebs.csi.aws.comparameters: type: gp3 fsType: ext4reclaimPolicy: DeletevolumeBindingMode: WaitForFirstConsumer
PersistentVolumeClaim
Request 10Gi of storage from the fast-ssd StorageClass.
apiVersion: v1kind: PersistentVolumeClaimmetadata: name: data-pvcspec: accessModes: - ReadWriteOnce storageClassName: fast-ssd resources: requests: storage: 10Gi
Mounting a PVC in a Pod
Attach the PVC as a volume inside a container.
apiVersion: v1kind: Podmetadata: name: appspec: containers: - name: app image: myapp:1.0 volumeMounts: - mountPath: /data name: data-volume volumes: - name: data-volume persistentVolumeClaim: claimName: data-pvc
Reclaim Policies
What happens to a PV when its claim is deleted.
- Retain- PV and underlying data are kept, must be manually reclaimed/deleted
- Delete- PV and the underlying storage asset (e.g. EBS volume) are deleted automatically
- Recycle (deprecated)- Basic scrub (`rm -rf`) then made available again, removed in newer Kubernetes versions
StatefulSet volumeClaimTemplates
Give each StatefulSet replica its own dynamically provisioned, stably-named PVC.
apiVersion: apps/v1kind: StatefulSetmetadata: name: postgresspec: 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: 20Gi# Produces PVCs named data-postgres-0, data-postgres-1, data-postgres-2# that survive pod rescheduling and are NOT deleted on scale-down by default.
Online Volume Expansion
Grow a bound PVC in place when the StorageClass allows it, without recreating the pod's data.
# StorageClass must set allowVolumeExpansion: truekubectl patch storageclass fast-ssd \ -p '{"allowVolumeExpansion": true}'# Edit the PVC's requested size upward (shrinking is not supported)kubectl patch pvc data-pvc -p '{"spec":{"resources":{"requests":{"storage":"50Gi"}}}}'# Watch the resize condition and then the filesystem grow inside the podkubectl get pvc data-pvc -o jsonpath='{.status.conditions}'# For most CSI drivers, the node driver auto-resizes the filesystem on# next mount; some in-tree drivers required a pod restart pre-1.24 CSI GA.
CSI Volume Snapshots
Take a point-in-time snapshot of a PVC's backing volume and restore it into a new PVC.
apiVersion: snapshot.storage.k8s.io/v1kind: VolumeSnapshotClassmetadata: name: csi-snapdriver: ebs.csi.aws.comdeletionPolicy: Delete---apiVersion: snapshot.storage.k8s.io/v1kind: VolumeSnapshotmetadata: name: data-snap-2026-07-21spec: volumeSnapshotClassName: csi-snap source: persistentVolumeClaimName: data-pvc---# Restore into a fresh PVC from the snapshotapiVersion: v1kind: PersistentVolumeClaimmetadata: name: data-restoredspec: storageClassName: fast-ssd dataSource: name: data-snap-2026-07-21 kind: VolumeSnapshot apiGroup: snapshot.storage.k8s.io accessModes: ["ReadWriteOnce"] resources: requests: storage: 10Gi
Local PVs & Generic Ephemeral Volumes
Bind directly to node-local disk for latency-sensitive workloads, or request per-pod ephemeral storage inline.
# Statically defined local PV - requires a nodeAffinity so the scheduler# co-locates the pod with the disk; no network hop, but no HA on node loss.apiVersion: v1kind: PersistentVolumemetadata: name: local-pv-1spec: capacity: storage: 100Gi volumeMode: Filesystem accessModes: ["ReadWriteOnce"] persistentVolumeReclaimPolicy: Delete storageClassName: local-storage local: path: /mnt/disks/ssd1 nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: ["node-3"]---# Generic ephemeral volume: PVC lifecycle tied 1:1 to the pod, defined inlineapiVersion: v1kind: Podmetadata: name: scratch-podspec: containers: - name: app image: myapp:1.0 volumeMounts: - mountPath: /scratch name: scratch volumes: - name: scratch ephemeral: volumeClaimTemplate: spec: accessModes: ["ReadWriteOnce"] storageClassName: fast-ssd resources: requests: storage: 5Gi
Storage Troubleshooting Cheatsheet
Fast diagnosis for the most common PV/PVC failure modes.
- PVC stuck Pending- No PV matches size/accessMode/storageClass, or `WaitForFirstConsumer` is waiting on a pod to be scheduled first
- `kubectl describe pvc <name>`- Shows binding events and provisioner errors; check the Events section first
- `kubectl get events --field-selector involvedObject.name=<pvc>`- Surfaces CSI provisioner failures (quota, AZ mismatch, IAM permissions)
- Pod stuck ContainerCreating with volume errors- Usually a node-side CSI mount failure; check `kubectl describe pod` and the csi-node daemonset logs
- Multi-Attach error for volume- A ReadWriteOnce volume is still attached to the old node; happens on ungraceful node loss, needs force-detach or `kubectl delete pod --force`
- storageClassName: "" on a PVC- Explicitly opts OUT of dynamic provisioning; only binds to a pre-existing PV with no storageClassName
Use `volumeBindingMode: WaitForFirstConsumer` on your StorageClass so the PV is provisioned in the same availability zone as the pod that will use it, avoiding cross-zone scheduling failures.