Master Kubernetes persistent storage architecture. Learn how the Container Storage Interface (CSI) communicates with cloud block storage, configure dynamic provisioning via StorageClasses, deploy resilient databases with StatefulSets, and perform point-in-time volume snapshots.
sequenceDiagram
autonumber
actor Admin as Operator / Developer
participant K8s as kube-apiserver
participant SC as StorageClass (CSI Provisioner)
participant Cloud as Storage Provider (AWS EBS / GCP PD / Ceph)
participant Kubelet as Node Kubelet (CSI Node Plugin)
Admin->>K8s: kubectl apply -f pvc.yaml (requests 50Gi)
K8s->>SC: Watch trigger: Unbound PVC detected
SC->>Cloud: CreateVolume RPC (Allocate 50Gi block storage)
Cloud-->>SC: Volume ID: vol-098234ab created
SC->>K8s: Create PersistentVolume (PV) & bind to PVC
Note over K8s,Kubelet: Pod scheduled on worker-node-02
K8s->>Cloud: AttachVolume RPC (Attach disk to worker-node-02)
Cloud-->>Kubelet: Block device attached (/dev/xvdf)
Kubelet->>Kubelet: Format ext4 & mount to /var/lib/kubelet/pods/.../volumes
Kubelet->>K8s: Container started with volume mounted!
1. Storage Fundamentals: PV, PVC, and Access Modes
Kubernetes decouples storage definition from consumption:
- PersistentVolume (PV): A piece of storage in the cluster provisioned by an administrator or dynamically by a StorageClass.
- PersistentVolumeClaim (PVC): A user request for storage (capacity and access mode) without needing to know underlying hardware details.
stateDiagram-v2
[*] --> Available: PV created by Admin or CSI
Available --> Bound: PVC matches PV size & access mode
Bound --> Released: User deletes PVC (ReclaimPolicy: Retain)
Released --> Available: Admin scrubs data & clears claimRef
Bound --> Deleted: User deletes PVC (ReclaimPolicy: Delete)
Deleted --> [*]: Physical volume deleted from cloud
Access Modes Comparison
| Mode | Short Code | Supported Providers | Description |
|---|---|---|---|
ReadWriteOnce |
RWO |
EBS, GCP PD, Azure Disk | Can be mounted as read-write by a single node. |
ReadOnlyMany |
ROX |
NFS, EFS, CephFS | Can be mounted read-only by many nodes simultaneously. |
ReadWriteMany |
RWX |
NFS, CephFS, Azure Files | Can be mounted as read-write by many nodes simultaneously. |
ReadWriteOncePod |
RWOP |
CSI 1.27+ | Can be mounted as read-write by a single Pod only. |
Step 1.1: Static Provisioning & Reclaim Policy Test
Create a static PV with Retain policy:
apiVersion: v1
kind: PersistentVolume
metadata:
name: static-pv-5gi
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
hostPath:
path: /tmp/data-storage
Create a PVC requesting this storage:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: static-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: manual
resources:
requests:
storage: 5Gi
Apply and verify binding:
kubectl apply -f static-pv.yaml
kubectl apply -f static-pvc.yaml
# Observe Status: Bound
kubectl get pv,pvc
Step 1.2: Test Retain Lifecycle
Delete the claim and observe that data is preserved:
kubectl delete pvc static-pvc
kubectl get pv static-pv-5gi
Status changes to Released. The underlying data in /tmp/data-storage is not deleted, protecting against accidental data loss.
2. Dynamic Provisioning with StorageClasses
In modern clusters, manual PV creation does not scale. A StorageClass defines a provisioner and parameters to create PVs on-demand when a PVC arrives.
flowchart TD
PVC["PVC: app-pvc (storage: 20Gi)"] --> SC["StorageClass: standard"]
SC --> Mode{"volumeBindingMode"}
Mode -->|Immediate| Fail["Creates PV immediately<br/>May place disk in Zone-A while Pod is scheduled in Zone-B!"]
Mode -->|WaitForFirstConsumer| Success["Waits until Pod is scheduled<br/>Creates PV in exact AZ where Pod runs!"]
Recommended StorageClass: WaitForFirstConsumer
Create storageclass-topology.yaml:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-storage
provisioner: kubernetes.io/no-provisioner # Replace with your CSI (e.g. ebs.csi.aws.com)
# CRITICAL: Prevents cross-availability zone scheduling failures
volumeBindingMode: WaitForFirstConsumer
# Enables increasing volume size without recreating pods
allowVolumeExpansion: true
reclaimPolicy: Delete
Apply:
kubectl apply -f storageclass-topology.yaml
Step 2.1: Online Volume Expansion
When an application runs out of disk space, Kubernetes allows expanding the volume without downtime if the StorageClass supports allowVolumeExpansion: true:
# Edit PVC capacity directly
kubectl patch pvc data-pvc -p '{"spec":{"resources":{"requests":{"storage":"100Gi"}}}}'
# Inspect PVC status for FileSystemResizePending
kubectl describe pvc data-pvc
Kubelet automatically resizes the underlying filesystem (ext4 or xfs) while the pod is running.
3. StatefulSets with volumeClaimTemplates
Deploying databases (MySQL, MongoDB, PostgreSQL, Kafka) using Deployments is an anti-pattern: all pods would attempt to mount the same single persistent volume, causing multi-attach lock errors.
A StatefulSet provides:
- Stable, unique network identifiers:
db-0,db-1,db-2. - Dedicated, persistent storage per pod:
data-db-0,data-db-1,data-db-2. - Ordered graceful deployment and termination: Pod 0 must be ready before Pod 1 starts; Pod 1 terminates before Pod 0 terminates.
flowchart TD
subgraph StatefulSet["StatefulSet: redis (replicas: 3)"]
Pod0["Pod: redis-0"] <--> PVC0["PVC: data-redis-0"] <--> PV0["PV: 10Gi (Dedicated)"]
Pod1["Pod: redis-1"] <--> PVC1["PVC: data-redis-1"] <--> PV1["PV: 10Gi (Dedicated)"]
Pod2["Pod: redis-2"] <--> PVC2["PVC: data-redis-2"] <--> PV2["PV: 10Gi (Dedicated)"]
end
Headless["Headless Service (redis-service)<br/>redis-0.redis-service.default.svc.cluster.local"] -. routes to .-> Pod0
Headless -. routes to .-> Pod1
Headless -. routes to .-> Pod2
Complete StatefulSet Manifest
Create redis-statefulset.yaml:
apiVersion: v1
kind: Service
metadata:
name: redis-headless
labels:
app: redis
spec:
ports:
- port: 6379
name: redis
clusterIP: None
selector:
app: redis
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
spec:
serviceName: "redis-headless"
replicas: 3
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
name: redis
volumeMounts:
- name: data
mountPath: /data
# Unique volume automatically created for EACH replica
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "fast-storage"
resources:
requests:
storage: 10Gi
Apply and inspect the ordered creation:
kubectl apply -f redis-statefulset.yaml
# Watch pods spawn in strict sequence (0 -> 1 -> 2)
kubectl get pods -l app=redis -w
# Check generated PVCs: data-redis-0, data-redis-1, data-redis-2
kubectl get pvc -l app=redis
4. Volume Snapshots & Restoring from Snapshots
Volume snapshots allow capturing point-in-time backups of persistent disks using the CSI driver.
flowchart LR
PVC["Live PVC: database-pvc"] -->|Trigger Snapshot| Snap["VolumeSnapshot: db-backup-2026"]
Snap -->|Create PVC with dataSource| RestoredPVC["New PVC: db-restored-pvc"]
RestoredPVC --> NewPod["New Pod: test-recovery-pod"]
Step 4.1: Take a Point-in-Time Snapshot
Create snapshot.yaml:
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: redis-snapshot-point-in-time
spec:
volumeSnapshotClassName: csi-aws-vsc
source:
persistentVolumeClaimName: data-redis-0
Apply and verify readiness:
kubectl apply -f snapshot.yaml
kubectl get volumesnapshot redis-snapshot-point-in-time
Expected output:
NAME READYTOUSE SOURCEPVC RESTORESIZE
redis-snapshot-point-in-time true data-redis-0 10Gi
Step 4.2: Restore from Snapshot into a New PVC
Create restore-pvc.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: redis-restored-pvc
spec:
storageClassName: fast-storage
dataSource:
name: redis-snapshot-point-in-time
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
Apply and verify:
kubectl apply -f restore-pvc.yaml
kubectl get pvc redis-restored-pvc
The new PVC contains an exact duplicate of the snapshot's filesystem ready to be attached to any recovery pod.
5. Storage Troubleshooting Diagnostic Scenarios
Diagnostic Decision Flowchart
flowchart TD
Issue["Pod stuck in ContainerCreating / Pending"] --> CheckDescribe["kubectl describe pod <name>"]
CheckDescribe --> Reason{"Inspect Events"}
Reason -->|FailedAttachVolume / Multi-Attach error| MultiAttach["Previous node did not release disk lock.<br/>Check if old pod terminated cleanly."]
Reason -->|FailedScheduling (unbound PVC)| CheckSC["Check StorageClass & CSI provisioner logs"]
Reason -->|Node affinity mismatch| AZMismatch["Disk exists in Zone-A, but node is in Zone-B.<br/>Fix with volumeBindingMode: WaitForFirstConsumer"]
Scenario: Resolving Multi-Attach Error
When a worker node crashes abruptly, cloud providers may keep the volume attached to the dead instance:
Warning FailedAttachVolume 2m attachdetach-controller
Multi-Attach error for volume "pvc-xxxx" Volume is already exclusively attached to node worker-01
Resolution:
- Check VolumeAttachment object:
kubectl get volumeattachments - If
worker-01is confirmed dead or stopped, force deletion of the orphaned Pod or clean up the VolumeAttachment:kubectl delete pod <stuck-pod> --grace-period=0 --force
Summary Checklist
- Use
ReadWriteOncefor block storage andReadWriteManyfor shared network filesystems (NFS). - Use
persistentVolumeReclaimPolicy: Retainon mission-critical databases to prevent accidental deletion. - Always set
volumeBindingMode: WaitForFirstConsumeron StorageClasses in multi-AZ clusters. - Enable
allowVolumeExpansion: trueto permit online disk resizing without application downtime. - Deploy databases using StatefulSets with
volumeClaimTemplatesto guarantee dedicated per-replica storage. - Capture consistent backups using the Kubernetes VolumeSnapshot API.