Kubernetes Storage — PV, PVC, StorageClasses, and CSI Drivers
You deploy a PostgreSQL pod. It works great until the pod restarts and all your data vanishes. Containers are ephemeral by design — their filesystem dies with them. Kubernetes storage solves this by letting you attach durable disks that survive pod restarts, rescheduling, and even node failures. But the storage system has layers, and understanding them saves you from painful data loss.
Ephemeral vs Persistent Storage
By default, every container gets a writable filesystem layer that exists only for the life of the container. When the container restarts, that layer is gone.
# Prove it: write a file, kill the pod, check if it survived
kubectl run test --image=busybox -- sh -c 'echo "important data" > /tmp/data.txt && sleep 3600'
# Verify the file exists
kubectl exec test -- cat /tmp/data.txt
# important data
# Delete and recreate
kubectl delete pod test
kubectl run test --image=busybox -- sh -c 'cat /tmp/data.txt 2>/dev/null || echo "DATA GONE"; sleep 3600'
kubectl logs test
# DATA GONE
For data that must survive, you need volumes.
Volume Types at a Glance
| Volume Type | Persistence | Use Case |
|---|---|---|
| emptyDir | Pod lifetime only | Scratch space, shared data between containers in a pod |
| hostPath | Node lifetime | Single-node testing, accessing node-level files |
| configMap / secret | Managed by K8s | Configuration injection |
| PersistentVolume (PV) | Independent of pod | Databases, file uploads, stateful apps |
| NFS | Network attached | Shared storage across pods |
| awsElasticBlockStore | Cloud managed | AWS workloads (legacy, use CSI instead) |
| azureDisk | Cloud managed | Azure workloads (legacy, use CSI instead) |
| csi | Depends on driver | Modern standard for all storage backends |
emptyDir — Temporary Shared Storage
An emptyDir volume is created when a pod is assigned to a node and exists as long as the pod runs. It is perfect for sharing data between containers in the same pod.
apiVersion: v1
kind: Pod
metadata:
name: data-processor
spec:
containers:
- name: writer
image: busybox
command: ['sh', '-c', 'while true; do date >> /data/output.log; sleep 5; done']
volumeMounts:
- name: shared-data
mountPath: /data
- name: reader
image: busybox
command: ['sh', '-c', 'tail -f /data/output.log']
volumeMounts:
- name: shared-data
mountPath: /data
volumes:
- name: shared-data
emptyDir:
sizeLimit: 500Mi # Optional: limit disk usage
# medium: Memory # Use tmpfs (RAM disk) for speed
PersistentVolume (PV) and PersistentVolumeClaim (PVC)
The PV/PVC system separates storage provisioning from storage consumption:
- PersistentVolume (PV) — A piece of storage in the cluster, provisioned by an admin or dynamically by a StorageClass. Think of it as the actual disk.
- PersistentVolumeClaim (PVC) — A request for storage by a user. Think of it as a ticket that reserves a PV.
Static Provisioning (Admin Creates PV Manually)
# Step 1: Admin creates the PV
apiVersion: v1
kind: PersistentVolume
metadata:
name: postgres-pv
labels:
type: local
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /mnt/data/postgres
# Step 2: Developer creates a PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
# selector: # Optional: match specific PV by label
# matchLabels:
# type: local
# Step 3: Pod uses the PVC
apiVersion: v1
kind: Pod
metadata:
name: postgres
spec:
containers:
- name: postgres
image: postgres:15
env:
- name: POSTGRES_PASSWORD
value: mysecret
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
ports:
- containerPort: 5432
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: postgres-pvc
# Check the binding
kubectl get pv
# NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM
# postgres-pv 10Gi RWO Retain Bound default/postgres-pvc
kubectl get pvc
# NAME STATUS VOLUME CAPACITY ACCESS MODES
# postgres-pvc Bound postgres-pv 10Gi RWO
Access Modes
| Mode | Abbreviation | Description | Typical Use |
|---|---|---|---|
| ReadWriteOnce | RWO | Read-write by a single node | Databases (PostgreSQL, MySQL) |
| ReadOnlyMany | ROX | Read-only by multiple nodes | Shared config, static assets |
| ReadWriteMany | RWX | Read-write by multiple nodes | Shared file uploads (NFS, EFS) |
| ReadWriteOncePod | RWOP | Read-write by a single pod (K8s 1.22+) | Strict single-writer guarantee |
Not all storage backends support all modes. EBS only supports RWO. NFS and EFS support RWX.
Reclaim Policies
What happens to the PV when the PVC is deleted?
| Policy | Behavior | Use Case |
|---|---|---|
| Retain | PV is kept with data intact, must be manually cleaned | Production data you cannot lose |
| Delete | PV and underlying storage are deleted | Dev/test environments, dynamic provisioning |
| Recycle | Data is deleted (rm -rf /volume/*), PV is reused | Deprecated, do not use |
StorageClasses — Dynamic Provisioning
Manually creating PVs does not scale. StorageClasses let Kubernetes automatically provision storage when a PVC is created.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com # CSI driver
parameters:
type: gp3
iops: "5000"
throughput: "250"
encrypted: "true"
reclaimPolicy: Delete
allowVolumeExpansion: true # Allow PVC resizing
volumeBindingMode: WaitForFirstConsumer # Provision in the pod's AZ
# PVC referencing the StorageClass — PV is created automatically
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: fast-storage
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd # References the StorageClass
resources:
requests:
storage: 50Gi
# List available StorageClasses
kubectl get storageclass
# See the default StorageClass (marked with "(default)")
kubectl get sc
# NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE
# gp2 (default) kubernetes.io/aws-ebs Delete WaitForFirstConsumer
# fast-ssd ebs.csi.aws.com Delete WaitForFirstConsumer
CSI Drivers — The Modern Standard
Container Storage Interface (CSI) is the standard plugin mechanism for storage in Kubernetes. Every cloud provider and storage vendor has a CSI driver:
| Provider | CSI Driver | Volume Types |
|---|---|---|
| AWS | ebs.csi.aws.com | gp2, gp3, io1, io2 |
| AWS | efs.csi.aws.com | EFS (NFS-based, RWX) |
| Azure | disk.csi.azure.com | Standard SSD, Premium SSD, Ultra |
| Azure | file.csi.azure.com | Azure Files (SMB/NFS, RWX) |
| GCP | pd.csi.storage.gke.io | pd-standard, pd-ssd, pd-balanced |
| vSphere | csi.vsphere.vmware.com | VMDK |
# List installed CSI drivers
kubectl get csidrivers
# Check CSI nodes
kubectl get csinodes
Volume Snapshots
Take a point-in-time snapshot of a PVC for backups or cloning:
# Create a VolumeSnapshotClass
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: ebs-snapshot-class
driver: ebs.csi.aws.com
deletionPolicy: Delete
---
# Take a snapshot
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: postgres-snapshot-20250419
spec:
volumeSnapshotClassName: ebs-snapshot-class
source:
persistentVolumeClaimName: postgres-pvc
# Check snapshot status
kubectl get volumesnapshot
# NAME READYTOUSE SOURCEPVC AGE
# postgres-snapshot-20250419 true postgres-pvc 2m
# Restore from snapshot (create a new PVC from the snapshot)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc-restored
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd
resources:
requests:
storage: 50Gi
dataSource:
name: postgres-snapshot-20250419
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
Expanding PVCs
If your StorageClass has allowVolumeExpansion: true, you can grow PVCs without downtime:
# Edit the PVC to request more storage
kubectl patch pvc postgres-pvc -p '{"spec":{"resources":{"requests":{"storage":"100Gi"}}}}'
# Check expansion progress
kubectl describe pvc postgres-pvc | grep -A 5 Conditions
# Type: FileSystemResizePending → Resizing is in progress
# The filesystem expands automatically when the pod restarts (or live for some CSI drivers)
You can grow PVCs but you cannot shrink them. Plan your initial sizes carefully.
ConfigMap and Secret Volumes
These special volume types project ConfigMap/Secret data as files:
volumes:
- name: config
configMap:
name: app-config
defaultMode: 0644 # File permissions
items:
- key: app.conf
path: application.conf # Rename the file
- name: certs
secret:
secretName: tls-certs
defaultMode: 0400 # Read-only for owner
These volumes update automatically when the underlying ConfigMap or Secret changes (within the kubelet sync period, typically 60 seconds).
Next up: kubectl Mastery — 40 essential commands every Kubernetes admin needs to know, from basic operations to advanced debugging and cluster management.
