1. Volume Types Overview
| Volume | Lifetime | Use case |
|---|---|---|
emptyDir | Pod lifetime | Scratch space, sidecar log sharing |
configMap / secret | Pod lifetime | Config files, credentials as files |
hostPath | Node filesystem | Node-level daemons (avoid for apps) |
persistentVolumeClaim | Independent of Pod | Databases, any persistent data |
projected | Pod lifetime | Combine multiple sources into one mount |
emptyDir
volumes:
- name: cache
emptyDir:
medium: Memory # tmpfs - fast, not persisted to disk
sizeLimit: 256Mi
- name: scratch
emptyDir: {} # backed by node disk
configMap as volume
volumes:
- name: nginx-config
configMap:
name: nginx-config
items:
- key: nginx.conf
path: nginx.conf
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
readOnly: true
2. PersistentVolumes & PVCs
Storage in Kubernetes follows a two-layer model:
- PersistentVolume (PV) - cluster-level storage resource (like a disk)
- PersistentVolumeClaim (PVC) - Pod's request for storage (like a claim ticket)
┌─────────────┐ binds to ┌─────────────┐ used by ┌─────────────┐
│ PVC │ ──────────▶ │ PV │ ◀────────── │ Pod │
│ (request) │ │ (resource) │ │ (consumer) │
└─────────────┘ └─────────────┘ └─────────────┘
Static PV (admin pre-provisions)
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-nfs-001
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: nfs
nfs:
server: nfs.example.com
path: /exports/data
PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
namespace: production
spec:
accessModes:
- ReadWriteOnce
storageClassName: gp3
resources:
requests:
storage: 50Gi
Access modes
| Mode | Abbreviation | Meaning |
|---|---|---|
| ReadWriteOnce | RWO | One node can mount read-write (most block storage) |
| ReadOnlyMany | ROX | Many nodes read-only |
| ReadWriteMany | RWX | Many nodes read-write (NFS, EFS, CephFS) |
| ReadWriteOncePod | RWOP | Single Pod only (K8s 1.22+) |
Reclaim policies
Retain- PV kept after PVC deleted; data preserved, manual cleanup requiredDelete- PV and underlying storage deleted when PVC is deleted (default for dynamic)Recycle- deprecated; scrubs data and makes PV available again
Using PVC in a Pod
apiVersion: v1
kind: Pod
metadata:
name: postgres-0
spec:
containers:
- name: postgres
image: postgres:16
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
persistentVolumeClaim:
claimName: postgres-data
3. StorageClasses & Dynamic Provisioning
Dynamic provisioning automatically creates a PV when a PVC is created - no admin pre-provisioning needed. A StorageClass defines the provisioner and parameters.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gp3
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: ebs.csi.aws.com
parameters:
type: gp3
iops: "3000"
throughput: "125"
encrypted: "true"
volumeBindingMode: WaitForFirstConsumer # delay binding until Pod scheduled
allowVolumeExpansion: true
reclaimPolicy: Delete
volumeBindingMode
Immediate- PV provisioned and bound as soon as PVC is createdWaitForFirstConsumer- waits until a Pod using the PVC is scheduled, then provisions in the correct AZ (recommended for cloud block storage)
# List storage classes
kubectl get storageclass
# Check PVC status
kubectl get pvc -A
kubectl describe pvc postgres-data
# Expand PVC (requires allowVolumeExpansion: true)
kubectl patch pvc postgres-data -p '{"spec":{"resources":{"requests":{"storage":"100Gi"}}}}'
4. CSI Drivers
The Container Storage Interface (CSI) is the standard plugin API for storage vendors. CSI drivers run as Pods in the cluster and handle create/mount/snapshot/resize operations.
# Common CSI drivers
# AWS: ebs.csi.aws.com, efs.csi.aws.com
# GCP: pd.csi.storage.gke.io
# Azure: disk.csi.azure.com, file.csi.azure.com
# NFS: nfs.csi.k8s.io
# Check CSI driver pods
kubectl get pods -n kube-system | grep csi
# VolumeSnapshot (backup a PVC)
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: postgres-snap-20240101
spec:
volumeSnapshotClassName: csi-aws-vsc
source:
persistentVolumeClaimName: postgres-data
Restore from snapshot by creating a new PVC with dataSource:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data-restored
spec:
storageClassName: gp3
dataSource:
name: postgres-snap-20240101
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
5. StatefulSet Storage Patterns
StatefulSets provide stable network identity and stable storage per replica via volumeClaimTemplates.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: gp3
resources:
requests:
storage: 50Gi
Each Pod gets its own PVC: data-postgres-0, data-postgres-1, data-postgres-2. PVCs persist even if the StatefulSet is deleted (unless you delete them manually).
6. Backup & Restore
Volume snapshots (CSI)
kubectl get volumesnapshot
kubectl get volumesnapshotclass
# Create snapshot
kubectl apply -f postgres-snapshot.yaml
# Restore to new PVC (see CSI section above)
Velero (cluster-level backup)
Velero backs up Kubernetes resources and PV snapshots to object storage (S3, GCS). Supports scheduled backups and disaster recovery.
# Install Velero (AWS example)
velero install \
--provider aws \
--bucket my-velero-backups \
--backup-location-config region=us-east-1 \
--snapshot-location-config region=us-east-1 \
--secret-file ./credentials-velero
# Backup a namespace
velero backup create prod-backup --include-namespaces production
# Restore
velero restore create --from-backup prod-backup
7. Troubleshooting
# PVC stuck in Pending?
kubectl describe pvc postgres-data
# Common causes: no StorageClass, no matching PV, insufficient capacity,
# WaitForFirstConsumer waiting for Pod, provisioner not running
# Pod stuck in ContainerCreating?
kubectl describe pod postgres-0
# Look for: FailedMount, FailedAttachVolume, Multi-Attach error
# Multi-Attach error (RWO volume on two nodes)
# Only one node can mount RWO volumes - ensure old Pod is fully terminated
# Check volume attachment
kubectl get volumeattachment
# CSI driver logs
kubectl logs -n kube-system -l app=ebs-csi-controller --tail=50
8. Cheat Sheet
| Task | Command |
|---|---|
| List PVCs | kubectl get pvc -A |
| List PVs | kubectl get pv |
| Storage classes | kubectl get sc |
| PVC details | kubectl describe pvc <name> |
| Expand PVC | kubectl patch pvc <name> -p '{"spec":{"resources":{"requests":{"storage":"100Gi"}}}}' |
| Snapshots | kubectl get volumesnapshot |
| StatefulSet PVCs | kubectl get pvc -l app=postgres |
Full cheat sheet: kubectl Cheat Sheet