Module #24 · intermediate Article
Storage Persistence: PersistentVolumes, PersistentVolumeClaims, and StorageClasses
A PersistentVolume (PV) is actual storage provisioned in the cluster; a PersistentVolumeClaim (PVC) is a pod's request for storage — Kubernetes binds them to...
5 min read+15 XPPublished 2026-07-26
Quick Answer: A PersistentVolume (PV) is actual storage provisioned in the cluster; a PersistentVolumeClaim (PVC) is a pod's request for storage — Kubernetes binds them together based on size, access mode, and storage class.
Detailed Answer:
The storage abstraction chain:
StorageClass (HOW to provision — AWS EBS gp3, EFS, etc.)
│
│ dynamically provisions
▼
PersistentVolume (WHAT storage exists — the actual EBS volume)
│
│ bound 1:1 via claim
▼
PersistentVolumeClaim (WHO wants storage — namespace-scoped request)
│
│ mounted into
▼
Pod (via volumes + volumeMounts)
# StorageClass — defines the provisioner and parameters
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gp3-encrypted
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: ebs.csi.aws.com
parameters:
type: gp3
fsType: ext4
encrypted: "true"
kmsKeyId: arn:aws:kms:us-east-1:123456789012:key/my-key
reclaimPolicy: Retain # CRITICAL for prod: Retain | Delete
volumeBindingMode: WaitForFirstConsumer # Provision in same AZ as pod
allowVolumeExpansion: true # Allow PVC resize without downtime
---
# PersistentVolumeClaim — what the pod requests
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
namespace: production
spec:
accessModes:
- ReadWriteOnce # RWO: one node at a time
storageClassName: gp3-encrypted
resources:
requests:
storage: 100Gi
# Optional: bind to specific PV by name
# volumeName: pv-postgres-prod
---
# Pod consuming the PVC
apiVersion: v1
kind: Pod
metadata:
name: postgres
spec:
containers:
- name: postgres
image: postgres:16
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
persistentVolumeClaim:
claimName: postgres-data # References the PVC by name
PVC Binding Process:
# Check PVC status
kubectl get pvc -n production
# NAME STATUS VOLUME CAPACITY
# postgres-data Bound pvc-a1b2c3d4-... 100Gi
# STATUS values:
# Pending → No matching PV yet (dynamic provisioning in progress, or no match)
# Bound → Successfully matched and mounted to a PV
# Lost → PV was deleted while PVC still exists
# Check what PV was created
kubectl get pv
kubectl describe pv pvc-a1b2c3d4-...
# Expand a PVC (StorageClass must have allowVolumeExpansion: true)
kubectl patch pvc postgres-data \
-p '{"spec":{"resources":{"requests":{"storage":"200Gi"}}}}'
ReclaimPolicy — Critical for Production:
# Delete (default for dynamic provisioning):
# PVC deleted → PV deleted → EBS volume DELETED → DATA LOST
# Retain:
# PVC deleted → PV status becomes Released → EBS volume KEPT
# You must manually reclaim: delete PV object, EBS volume persists
# Always use Retain for production databases
# Change existing StorageClass:
kubectl patch storageclass gp3 \
-p '{"reclaimPolicy":"Retain"}'
Common Mistakes:
- Using
reclaimPolicy: Deletefor production databases — onekubectl delete pvcdestroys your data - Not setting
volumeBindingMode: WaitForFirstConsumer— PVC binds in wrong AZ, pod can't mount - Using
ReadWriteManywith EBS — EBS only supportsReadWriteOnce(use EFS for RWX)
Key Takeaway: Always set reclaimPolicy: Retain on StorageClasses used for production data — the default Delete policy means a mistaken kubectl delete pvc permanently destroys your data.
Finished reading?
Mark as complete to claim your +15 XP and track progress.
