Skip to content
Codeloom
Kubernetes

Kubernetes Persistent Volumes and Storage Classes

Understand PersistentVolumes, PersistentVolumeClaims, and StorageClasses to give your Kubernetes workloads reliable, portable storage.

·6 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • The relationship between PVs, PVCs, and StorageClasses
  • Static vs dynamic provisioning
  • Access modes and reclaim policies
  • Using StorageClasses for different performance tiers
  • Practical patterns for databases and stateful workloads

Prerequisites

None — this post is self-contained.

Containers are ephemeral by design. When a pod restarts, its filesystem resets. Databases, file uploads, and any workload that needs data to survive restarts requires persistent storage. Kubernetes models this with three resources: PersistentVolumes, PersistentVolumeClaims, and StorageClasses.

The Three Resources

PersistentVolume (PV) represents a piece of storage in the cluster. It could be a disk on a cloud provider, an NFS share, or a local SSD. A PV exists independently of any pod.

PersistentVolumeClaim (PVC) is a request for storage by a user. It specifies how much space is needed and what access mode is required. Kubernetes binds a PVC to a matching PV.

StorageClass defines how storage is dynamically provisioned. Instead of pre-creating PVs, you define a StorageClass and let Kubernetes create PVs on demand when PVCs are submitted.

The mental model: a PV is the actual disk, a PVC is a ticket requesting a disk, and a StorageClass is the vending machine that creates disks automatically.

Static Provisioning

In static provisioning, a cluster administrator creates PVs ahead of time:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: data-pv
spec:
  capacity:
    storage: 50Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  hostPath:
    path: /mnt/data

A developer then creates a PVC that matches:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 50Gi

Kubernetes binds the PVC to the PV based on capacity, access mode, and optional label selectors. The pod mounts the PVC:

apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  containers:
    - name: app
      image: myapp:latest
      volumeMounts:
        - name: data
          mountPath: /var/lib/app/data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: data-pvc

Static provisioning works but does not scale. Every PVC requires a pre-created PV. Dynamic provisioning solves this.

Dynamic Provisioning with StorageClasses

A StorageClass tells Kubernetes how to create PVs automatically. On AWS EKS:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  iops: "5000"
  throughput: "250"
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

On Google GKE:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: pd.csi.storage.gke.io
parameters:
  type: pd-ssd
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

Now a PVC references the StorageClass, and Kubernetes provisions the disk automatically:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: db-data
spec:
  storageClassName: fast-ssd
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Gi

When this PVC is created, the CSI driver provisions a 100 GB SSD, creates a PV object, and binds it to the PVC. No administrator intervention needed.

Access Modes

Access modes define how many nodes can mount a volume simultaneously:

ModeAbbreviationDescription
ReadWriteOnceRWOOne node can mount read-write
ReadOnlyManyROXMany nodes can mount read-only
ReadWriteManyRWXMany nodes can mount read-write
ReadWriteOncePodRWOPOne pod can mount read-write

Most block storage (EBS, Persistent Disk) supports only ReadWriteOnce. For ReadWriteMany, you need a filesystem-based solution like NFS, EFS, or CephFS.

ReadWriteOncePod (Kubernetes 1.27+) is stricter than ReadWriteOnce. RWO allows multiple pods on the same node to mount the volume. RWOP ensures exactly one pod across the entire cluster has write access, which is safer for databases.

Reclaim Policies

When a PVC is deleted, the reclaim policy on the PV determines what happens to the underlying storage:

  • Delete removes the PV and the backing storage (the actual cloud disk). This is the default for dynamically provisioned volumes.
  • Retain keeps the PV and storage intact. An administrator must manually clean up or rebind the PV.

For production databases, use Retain to prevent accidental data loss:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: database-storage
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer

Volume Binding Mode

volumeBindingMode controls when provisioning happens:

  • Immediate provisions the disk as soon as the PVC is created, even if no pod uses it yet. The disk is created in a random availability zone.
  • WaitForFirstConsumer delays provisioning until a pod that uses the PVC is scheduled. The disk is created in the same availability zone as the node.

Always use WaitForFirstConsumer in multi-zone clusters. Without it, the disk might be provisioned in zone A while the pod is scheduled in zone B, and the pod will be stuck in Pending.

Volume Expansion

If a StorageClass has allowVolumeExpansion: true, you can grow a PVC by editing its spec:

kubectl patch pvc db-data -p '{"spec":{"resources":{"requests":{"storage":"200Gi"}}}}'

Kubernetes expands the underlying disk and, for most filesystem types, resizes the filesystem online. Shrinking is not supported.

StatefulSets and Volume Claim Templates

StatefulSets pair naturally with persistent volumes. Each replica gets its own PVC through a volume claim template:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  replicas: 3
  serviceName: postgres
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        storageClassName: fast-ssd
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 50Gi

This creates PVCs named data-postgres-0, data-postgres-1, and data-postgres-2. Each pod gets its own dedicated disk. When scaling up, new PVCs are created automatically. When scaling down, PVCs are preserved so data is not lost if you scale back up.

Multiple Storage Tiers

A practical cluster offers multiple StorageClasses for different workloads:

# Standard tier for general workloads
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
---
# High-performance tier for databases
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: high-iops
provisioner: ebs.csi.aws.com
parameters:
  type: io2
  iops: "10000"
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

Developers choose the appropriate tier in their PVC’s storageClassName field.

Practical Recommendations

Use dynamic provisioning with StorageClasses for all new workloads. Set volumeBindingMode: WaitForFirstConsumer to avoid zone mismatch issues. Use reclaimPolicy: Retain for any storage class used by databases or other critical stateful applications. Pair StatefulSets with volume claim templates for replicated stateful workloads. Enable allowVolumeExpansion so you can grow disks without recreating PVCs.