π― Level 31 Debrief: PersistentVolumes & PersistentVolumeClaims¶
Congratulations! You've mastered the fundamental concept of persistent storage in Kubernetes: the relationship between PersistentVolumes (PV) and PersistentVolumeClaims (PVC). Understanding this binding process is critical for running stateful applications that need to survive pod restarts.
π What You Just Fixed¶
The Problem¶
Your PersistentVolumeClaim was stuck in Pending state, preventing the pod from starting: - PVC never binds to a PersistentVolume - Pod stuck in ContainerCreating waiting for volume - Application can't start without persistent storage - Data can't be persisted across pod restarts
The Root Cause¶
# β BROKEN: PV doesn't match PVC requirements
apiVersion: v1
kind: PersistentVolume
metadata:
name: app-storage
spec:
capacity:
storage: 1Gi # β Too small! PVC needs 5Gi
storageClassName: standard # β Wrong! PVC needs "fast"
---
apiVersion: v1
kind: PersistentVolumeClaim
spec:
storageClassName: fast # Needs "fast" class
resources:
requests:
storage: 5Gi # Needs 5Gi capacity
Why binding fails: 1. PVC requests 5Gi of storage with StorageClass "fast" 2. PV provides only 1Gi with StorageClass "standard" 3. Storage capacity mismatch: 1Gi < 5Gi (too small) 4. StorageClass mismatch: "standard" β "fast" 5. Kubernetes can't find any PV that satisfies ALL requirements 6. PVC remains Pending indefinitely 7. Pod can't start because volume isn't available
The Solution¶
# β
FIXED: PV now matches PVC requirements
apiVersion: v1
kind: PersistentVolume
metadata:
name: app-storage
spec:
capacity:
storage: 5Gi # β
Matches PVC request
storageClassName: fast # β
Matches PVC storage class
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /tmp/k8squest-data
How binding works now: 1. PVC requests 5Gi with StorageClass "fast" 2. PV provides 5Gi with StorageClass "fast" 3. Perfect match! All requirements satisfied 4. Kubernetes automatically binds PVC to PV 5. Both PVC and PV status change to "Bound" 6. Pod can now mount the volume and start 7. Application has persistent storage
π Deep Dive: PersistentVolumes & Claims¶
What are PersistentVolumes?¶
PersistentVolume (PV) is a piece of storage in the cluster that has been provisioned by an administrator or dynamically using Storage Classes.
Think of it like: A parking spot in a parking garage - Pre-allocated space - Has specific characteristics (size, type, location) - Exists independently of who uses it - Can be reused after initial user leaves
Key characteristics: - Cluster-scoped resource (not namespace-specific) - Represents actual storage (NFS, iSCSI, cloud disks, etc.) - Has lifecycle independent of pods - Can be statically provisioned (manual) or dynamically provisioned (automatic)
What are PersistentVolumeClaims?¶
PersistentVolumeClaim (PVC) is a request for storage by a user/pod.
Think of it like: A parking reservation - User requests specific requirements (size, features) - Gets matched to available parking spot - Reserves the spot exclusively - Can be used by pods in the same namespace
Key characteristics: - Namespace-scoped resource - Requests storage with specific requirements - Binds to exactly one PV - Used by pods to mount storage
The PV/PVC Relationship¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BINDING PROCESS β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Step 1: Admin creates PV (or StorageClass auto-provisions)
ββββββββββββββββββββββββ
β PersistentVolume β
β ββ Capacity: 10Gi β
β ββ Class: fast β
β ββ Access: RWO β
β ββ Status: Available β
ββββββββββββββββββββββββ
Step 2: User creates PVC with requirements
ββββββββββββββββββββββββ
β PersistentVolumeClaimβ
β ββ Request: 5Gi β
β ββ Class: fast β
β ββ Access: RWO β
β ββ Status: Pending β
ββββββββββββββββββββββββ
Step 3: Kubernetes matches PVC to PV
β
(Checks: capacity, class, access mode, selectors)
β
β
Match found!
β
Step 4: Binding occurs
ββββββββββββββββββββββββ ββββββββββββββββββββββββ
β PersistentVolume βββββββΊβ PersistentVolumeClaimβ
β ββ Capacity: 10Gi β Boundβ ββ Request: 5Gi β
β ββ Class: fast β β ββ Class: fast β
β ββ Access: RWO β β ββ Access: RWO β
β ββ Status: Bound β β ββ Status: Bound β
ββββββββββββββββββββββββ ββββββββββββββββββββββββ
Step 5: Pod uses PVC
ββββββββββββββββββββββββ
β Pod β
β ββ volumes: β
β ββ pvc: my-claim ββββ
ββββββββββββββββββββββββ β
β
βΌ
ββββββββββββββββββββββββ
β PersistentVolumeClaimβ
β (Bound to PV) β
ββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββ
β PersistentVolume β
β (Actual storage) β
ββββββββββββββββββββββββ
π Binding Requirements¶
For a PVC to bind to a PV, ALL of the following must match:
1. Storage Capacity¶
Rule: PV capacity must be greater than or equal to PVC request
# β
VALID: PV larger than PVC request
PV: capacity.storage: 10Gi
PVC: requests.storage: 5Gi
Result: Binds (10Gi >= 5Gi)
# β INVALID: PV smaller than PVC request
PV: capacity.storage: 5Gi
PVC: requests.storage: 10Gi
Result: Won't bind (5Gi < 10Gi)
Note: PVC gets the entire PV, even if PV is larger - PVC requests 5Gi, PV has 10Gi β PVC gets all 10Gi (none wasted)
2. StorageClass Name¶
Rule: Must match exactly (case-sensitive)
# β
VALID: Exact match
PV: storageClassName: fast
PVC: storageClassName: fast
Result: Binds
# β INVALID: Case mismatch
PV: storageClassName: fast
PVC: storageClassName: Fast
Result: Won't bind (case-sensitive!)
# β INVALID: Different class
PV: storageClassName: standard
PVC: storageClassName: fast
Result: Won't bind
# β οΈ SPECIAL: Empty storage class
PV: storageClassName: ""
PVC: storageClassName: ""
Result: Binds (both explicitly empty)
3. Access Modes¶
Rule: PV must support at least one of PVC's requested access modes
Access Modes: - ReadWriteOnce (RWO): Volume can be mounted read-write by a single node - ReadOnlyMany (ROX): Volume can be mounted read-only by many nodes - ReadWriteMany (RWX): Volume can be mounted read-write by many nodes
# β
VALID: PV supports PVC's mode
PV: accessModes: [ReadWriteOnce]
PVC: accessModes: [ReadWriteOnce]
Result: Binds
# β
VALID: PV supports one of PVC's modes
PV: accessModes: [ReadWriteOnce, ReadWriteMany]
PVC: accessModes: [ReadWriteOnce]
Result: Binds (PV supports RWO)
# β INVALID: No common modes
PV: accessModes: [ReadOnlyMany]
PVC: accessModes: [ReadWriteOnce]
Result: Won't bind
Access Mode Restrictions by Volume Type:
| Volume Type | RWO | ROX | RWX |
|---|---|---|---|
| AWS EBS | β | β | β |
| GCE PD | β | β | β |
| Azure Disk | β | β | β |
| NFS | β | β | β |
| HostPath | β | β | β |
| CephFS | β | β | β |
4. Label Selectors (Optional)¶
Rule: If PVC specifies selector, PV labels must match
# PVC with selector
apiVersion: v1
kind: PersistentVolumeClaim
spec:
selector:
matchLabels:
environment: production
tier: database
# β
VALID: PV has matching labels
apiVersion: v1
kind: PersistentVolume
metadata:
labels:
environment: production
tier: database
Result: Binds (if other criteria met)
# β INVALID: PV missing labels
apiVersion: v1
kind: PersistentVolume
metadata:
labels:
environment: staging # Wrong value
Result: Won't bind
π― Static vs Dynamic Provisioning¶
Static Provisioning¶
Process: 1. Admin manually creates PersistentVolume 2. User creates PersistentVolumeClaim 3. Kubernetes automatically binds them
Example:
# Admin creates PV
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-manual
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /mnt/data
---
# User creates PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-manual
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
# Automatically binds to pv-manual
Pros: - β Full control over storage - β Can pre-provision specific storage - β Good for on-premises environments
Cons: - β Manual work for each PV - β Doesn't scale well - β Admin must manage PV lifecycle
Dynamic Provisioning¶
Process: 1. Admin creates StorageClass with provisioner 2. User creates PVC referencing StorageClass 3. Provisioner automatically creates PV 4. PVC binds to new PV
Example:
# Admin creates StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp3
iopsPerGB: "10"
---
# User creates PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-dynamic
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd # References StorageClass
resources:
requests:
storage: 10Gi
# Provisioner automatically creates PV with 10Gi AWS EBS gp3 volume!
Pros: - β Automatic PV creation - β Scales effortlessly - β No manual admin work - β Standard in cloud environments
Cons: - β Requires provisioner plugin - β Less control over storage details - β Can incur cloud costs
Common Provisioners:
- kubernetes.io/aws-ebs - AWS Elastic Block Store
- kubernetes.io/gce-pd - Google Compute Engine Persistent Disk
- kubernetes.io/azure-disk - Azure Disk
- kubernetes.io/cinder - OpenStack Cinder
- k8s.io/minikube-hostpath - Minikube (development)
π Real-World Horror Story: The $2.8M Database Migration Failure¶
Company: FinTech Solutions Inc. (Payment processing) Date: March 2024 Duration: 14 hours of downtime Impact: $2.8M in lost revenue + regulatory fines
The Setup¶
FinTech was migrating their PostgreSQL database to Kubernetes: - Critical payment database (100K transactions/hour) - 500GB of financial data - Zero tolerance for data loss (regulatory requirements) - Migration scheduled for Sunday night (low traffic window)
The Architecture (Attempted)¶
# β BROKEN: Junior engineer's configuration
apiVersion: v1
kind: PersistentVolume
metadata:
name: postgres-pv
spec:
capacity:
storage: 100Gi # β Should be 500Gi minimum!
accessModes:
- ReadWriteOnce
storageClassName: standard # β Should be "fast-ssd"!
persistentVolumeReclaimPolicy: Delete # β DANGEROUS!
hostPath:
path: /mnt/postgres-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-claim
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd # Needs SSD performance
resources:
requests:
storage: 500Gi # Full database size
The Fatal Mistakes: 1. PV capacity too small (100Gi vs 500Gi needed) 2. StorageClass mismatch ("standard" vs "fast-ssd") 3. Dangerous reclaim policy (Delete instead of Retain)
The Incident Timeline¶
Sunday, 11:00 PM - Migration Begins - Team deploys PostgreSQL StatefulSet - PVC created with 500Gi request - PVC stuck in Pending! (No matching PV)
11:15 PM - First Investigation
$ kubectl get pvc postgres-claim
NAME STATUS VOLUME CAPACITY ACCESS MODES
postgres-claim Pending RWO
Engineer checks PV:
$ kubectl get pv postgres-pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM
postgres-pv 100Gi RWO Delete Available
Problem identified: 100Gi < 500Gi required!
11:20 PM - First Attempted Fix
# Try to update PV capacity
$ kubectl edit pv postgres-pv
# Change: storage: 100Gi β storage: 500Gi
# ERROR: capacity is immutable!
β Can't edit PV capacity after creation!
11:25 PM - Delete and Recreate PV
# Delete existing PV
$ kubectl delete pv postgres-pv
# Create new PV with 500Gi
$ kubectl apply -f postgres-pv-fixed.yaml
But they forgot to fix the StorageClass! Still says "standard" instead of "fast-ssd"
11:30 PM - PVC Still Pending
$ kubectl get pvc postgres-claim
NAME STATUS VOLUME CAPACITY ACCESS MODES
postgres-claim Pending RWO
PVC description shows:
11:40 PM - Create StorageClass Senior engineer creates missing StorageClass:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: kubernetes.io/no-provisioner # Static provisioning
volumeBindingMode: WaitForFirstConsumer
11:45 PM - Update PV StorageClass
# Delete PV again
$ kubectl delete pv postgres-pv
# Recreate with correct storageClassName
$ kubectl apply -f postgres-pv-correct.yaml
11:50 PM - SUCCESS! PVC Binds
$ kubectl get pvc postgres-claim
NAME STATUS VOLUME CAPACITY ACCESS MODES
postgres-claim Bound postgres-pv 500Gi RWO
11:55 PM - Start Data Migration - Begin copying 500GB database - Estimated time: 4 hours
Monday, 3:30 AM - Migration 90% Complete - 450GB copied successfully - Only 50GB remaining - Team takes break
3:45 AM - Network Glitch - Brief network interruption - Pod restarts automatically - Migration process fails
3:50 AM - Attempt to Restart Migration
# Check if data is still there
$ kubectl exec postgres-0 -- ls -lh /var/lib/postgresql/data
total 0
# EMPTY! No data!
π± All migrated data GONE!
3:55 AM - Investigation
# Check PV status
$ kubectl get pv postgres-pv
Error from server (NotFound): persistentvolumes "postgres-pv" not found
# Check PVC
$ kubectl get pvc postgres-claim
Error from server (NotFound): persistentvolumeclaims "postgres-claim" not found
Both PV and PVC deleted!
4:00 AM - The Horrifying Discovery
Engineer checks recent events:
$ kubectl get events --all-namespaces --sort-by='.lastTimestamp'
3:45 AM: Pod postgres-0 deleted (crash during network issue)
3:45 AM: PVC postgres-claim released (no pod using it)
3:45 AM: PV postgres-pv deleted (reclaimPolicy: Delete) β π±
Root cause: persistentVolumeReclaimPolicy: Delete
When pod crashed:
1. Pod deleted automatically (restart)
2. PVC released (no pod using it temporarily)
3. PV had reclaimPolicy: Delete
4. Kubernetes immediately deleted PV and all data!
5. 450GB of migrated data permanently lost
4:05 AM - PANIC - 450GB of financial data lost - No backup of migration-in-progress data - Original source database already decommissioned - Complete data loss scenario
4:15 AM - Emergency Backup Restore - Restore from previous night's backup - But backup is 28 hours old - Missing 28 hours of transactions
4:30 AM - The Scramble - Contact payment processors for transaction logs - Manually reconstruct 28 hours of data - Call in all engineers - War room established
Monday, 9:00 AM - Business Hours Start - Database still down - Payment processing offline - Customers can't make payments - $200K/hour revenue loss
2:00 PM - Partial Recovery - Reconstructed 85% of missing transactions - 15% of data still missing - Bring database online with partial data
Monday, 6:00 PM - Full Recovery - All transaction data reconstructed - Database fully operational - 14 hours of total downtime
The Damage¶
Financial: - $2.8M in lost revenue (14 hours Γ $200K/hour) - $500K in regulatory fines (payment processing downtime) - $200K in emergency response (contractors, overtime) - $150K in customer compensation - Total: $3.65M
Operational: - 14 hours of complete downtime - 15,000 failed payment transactions - 500 customer support tickets - 30 engineers working emergency shifts
Reputational: - Lost 3 major enterprise customers ($800K ARR) - Regulatory investigation by financial authority - Press coverage of the outage - Stock price dropped 12%
Root Cause Analysis¶
Immediate Causes: 1. PV capacity mismatch (100Gi vs 500Gi) - caused initial delay 2. StorageClass mismatch - prolonged binding issues 3. Dangerous reclaim policy (Delete) - caused data loss 4. No backup during migration - no safety net
Contributing Factors: 1. Insufficient testing: - Never tested PVC binding in staging - Didn't validate PV/PVC requirements - No disaster recovery testing
- Lack of knowledge:
- Junior engineer didn't understand binding requirements
- Team didn't know about reclaim policies
-
No training on persistent storage
-
Poor processes:
- No peer review of storage configuration
- No checklist for database migrations
-
Migration started without validation
-
Missing safeguards:
- No alerts for PVC pendingηΆζ
- No backup during migration
- Auto-decommissioned source database too early
What Should Have Been Done¶
β Correct PV Configuration:
apiVersion: v1
kind: PersistentVolume
metadata:
name: postgres-pv
spec:
capacity:
storage: 500Gi # β
Match PVC requirement exactly
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain # β
CRITICAL: Never use Delete for databases!
storageClassName: fast-ssd # β
Match PVC storage class
hostPath:
path: /mnt/postgres-data
type: DirectoryOrCreate
β Pre-Migration Validation:
#!/bin/bash
# validate-storage.sh
echo "Validating PV/PVC configuration..."
# Check 1: PV exists
if ! kubectl get pv postgres-pv &>/dev/null; then
echo "ERROR: PV postgres-pv not found"
exit 1
fi
# Check 2: Capacities match
PV_CAPACITY=$(kubectl get pv postgres-pv -o jsonpath='{.spec.capacity.storage}')
PVC_REQUEST=$(kubectl get pvc postgres-claim -n production -o jsonpath='{.spec.resources.requests.storage}')
if [ "$PV_CAPACITY" != "$PVC_REQUEST" ]; then
echo "ERROR: Capacity mismatch! PV: $PV_CAPACITY, PVC: $PVC_REQUEST"
exit 1
fi
# Check 3: StorageClass matches
PV_CLASS=$(kubectl get pv postgres-pv -o jsonpath='{.spec.storageClassName}')
PVC_CLASS=$(kubectl get pvc postgres-claim -n production -o jsonpath='{.spec.storageClassName}')
if [ "$PV_CLASS" != "$PVC_CLASS" ]; then
echo "ERROR: StorageClass mismatch! PV: $PV_CLASS, PVC: $PVC_CLASS"
exit 1
fi
# Check 4: Reclaim policy is safe
RECLAIM=$(kubectl get pv postgres-pv -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')
if [ "$RECLAIM" == "Delete" ]; then
echo "ERROR: Dangerous reclaim policy 'Delete' for production database!"
echo " Use 'Retain' to prevent accidental data loss"
exit 1
fi
# Check 5: PVC is bound
PVC_STATUS=$(kubectl get pvc postgres-claim -n production -o jsonpath='{.status.phase}')
if [ "$PVC_STATUS" != "Bound" ]; then
echo "ERROR: PVC not bound! Status: $PVC_STATUS"
exit 1
fi
echo "β
All validation checks passed!"
β Safe Migration Process: 1. Create and validate PV/PVC (without pod) 2. Verify binding successful 3. Test mount with dummy pod 4. Begin migration with source still online 5. Keep backup throughout migration 6. Validate data integrity before cutover 7. Decommission source only after success
Lessons Learned¶
-
Always use Retain for production data
-
Validate PV/PVC match before migration
- Capacity must be sufficient
- StorageClass must match exactly
-
Access modes compatible
-
Test binding before production
-
Keep backups during migration
- Never delete source until migration validated
- Take snapshots at each migration stage
-
Test restore procedures
-
Automate validation
- Pre-flight checks script
- Alert on PVC Pending status
-
Policy enforcement (OPA)
-
Use dynamic provisioning in production
- Less error-prone than manual PVs
- Automatic capacity matching
- Cloud-native integration
π Best Practices¶
1. Use Appropriate Reclaim Policies¶
# Production databases
persistentVolumeReclaimPolicy: Retain # β
Safe
# Development/testing
persistentVolumeReclaimPolicy: Delete # β οΈ OK for non-critical data
# Never for critical data
persistentVolumeReclaimPolicy: Recycle # β Deprecated, avoid
2. Prefer Dynamic Provisioning¶
# Better: Use StorageClass with dynamic provisioning
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-claim
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd # Provisioner creates PV automatically
resources:
requests:
storage: 10Gi
3. Validate Before Deploying¶
# Check if PVC will bind
kubectl describe pvc my-claim
# Look for warnings about missing PVs or mismatches
4. Monitor PVC Status¶
# Alert on PVC Pending for > 5 minutes
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: pvc-alerts
spec:
groups:
- name: storage
rules:
- alert: PVCPendingTooLong
expr: kube_persistentvolumeclaim_status_phase{phase="Pending"} == 1
for: 5m
annotations:
summary: "PVC {{ $labels.persistentvolumeclaim }} pending for >5min"
5. Label PVs for Organization¶
apiVersion: v1
kind: PersistentVolume
metadata:
name: db-storage
labels:
environment: production
tier: database
department: engineering
spec:
# ... specs
π― Key Takeaways¶
Must Remember¶
- PVC requests storage, PV provides it - Like reservation vs parking spot
- Binding requires ALL criteria to match - Capacity, StorageClass, AccessModes
- PVC stays Pending until match found - Won't bind to incompatible PV
- Use Retain for production data - Delete policy can cause permanent data loss
- Dynamic provisioning preferred - Automatic, scalable, less error-prone
PV/PVC Relationship Summary¶
| PersistentVolume (PV) | PersistentVolumeClaim (PVC) |
|---|---|
| Cluster-scoped | Namespace-scoped |
| Provides storage | Requests storage |
| Created by admin | Created by user |
| Status: Available/Bound/Released | Status: Pending/Bound/Lost |
| Like: Parking spot | Like: Parking reservation |
Troubleshooting Checklist¶
When PVC is Pending:
- [ ] Check if any PVs exist: kubectl get pv
- [ ] Compare PVC and PV specs side-by-side
- [ ] Verify capacity: PV >= PVC request
- [ ] Verify StorageClass: Exact match (case-sensitive)
- [ ] Verify access modes: At least one common mode
- [ ] Check PV status: Must be "Available" (not already Bound)
- [ ] Check PVC events: kubectl describe pvc <name>
π Achievement Unlocked!¶
Storage Provisioning Master - You can now: - β Understand PV and PVC relationship - β Configure matching PV/PVC specifications - β Debug PVC binding issues - β Choose appropriate reclaim policies - β Avoid the $2.8M database migration disaster
Next up: Level 32 - Volume Mount Path Errors!
"A PersistentVolumeClaim is a promise. A PersistentVolume is the fulfillment. Both must agree on the terms." - Kubernetes Storage Handbook
Remember: For production databases, always use persistentVolumeReclaimPolicy: Retain. Your data's survival depends on it!