Kubernetes Storage

Persistent data for stateful workloads - from emptyDir to cloud block storage and CSI drivers.

1. Volume Types Overview

VolumeLifetimeUse case
emptyDirPod lifetimeScratch space, sidecar log sharing
configMap / secretPod lifetimeConfig files, credentials as files
hostPathNode filesystemNode-level daemons (avoid for apps)
persistentVolumeClaimIndependent of PodDatabases, any persistent data
projectedPod lifetimeCombine 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:

┌─────────────┐  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

ModeAbbreviationMeaning
ReadWriteOnceRWOOne node can mount read-write (most block storage)
ReadOnlyManyROXMany nodes read-only
ReadWriteManyRWXMany nodes read-write (NFS, EFS, CephFS)
ReadWriteOncePodRWOPSingle Pod only (K8s 1.22+)

Reclaim policies

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

# 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).

Scaling down: StatefulSet scale-down does NOT delete PVCs. You must manually delete orphaned PVCs if you want to reclaim storage.

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

TaskCommand
List PVCskubectl get pvc -A
List PVskubectl get pv
Storage classeskubectl get sc
PVC detailskubectl describe pvc <name>
Expand PVCkubectl patch pvc <name> -p '{"spec":{"resources":{"requests":{"storage":"100Gi"}}}}'
Snapshotskubectl get volumesnapshot
StatefulSet PVCskubectl get pvc -l app=postgres

Full cheat sheet: kubectl Cheat Sheet