Skip to main content

Kubernetes Storage — PV, PVC, StorageClasses, and CSI Drivers

· 6 min read
Goel Academy
DevOps & Cloud Learning Hub

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 TypePersistenceUse Case
emptyDirPod lifetime onlyScratch space, shared data between containers in a pod
hostPathNode lifetimeSingle-node testing, accessing node-level files
configMap / secretManaged by K8sConfiguration injection
PersistentVolume (PV)Independent of podDatabases, file uploads, stateful apps
NFSNetwork attachedShared storage across pods
awsElasticBlockStoreCloud managedAWS workloads (legacy, use CSI instead)
azureDiskCloud managedAzure workloads (legacy, use CSI instead)
csiDepends on driverModern 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

ModeAbbreviationDescriptionTypical Use
ReadWriteOnceRWORead-write by a single nodeDatabases (PostgreSQL, MySQL)
ReadOnlyManyROXRead-only by multiple nodesShared config, static assets
ReadWriteManyRWXRead-write by multiple nodesShared file uploads (NFS, EFS)
ReadWriteOncePodRWOPRead-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?

PolicyBehaviorUse Case
RetainPV is kept with data intact, must be manually cleanedProduction data you cannot lose
DeletePV and underlying storage are deletedDev/test environments, dynamic provisioning
RecycleData is deleted (rm -rf /volume/*), PV is reusedDeprecated, 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:

ProviderCSI DriverVolume Types
AWSebs.csi.aws.comgp2, gp3, io1, io2
AWSefs.csi.aws.comEFS (NFS-based, RWX)
Azuredisk.csi.azure.comStandard SSD, Premium SSD, Ultra
Azurefile.csi.azure.comAzure Files (SMB/NFS, RWX)
GCPpd.csi.storage.gke.iopd-standard, pd-ssd, pd-balanced
vSpherecsi.vsphere.vmware.comVMDK
# 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.