Kubernetes

Kubernetes Storage: PVCs, StorageClasses & Dynamic Provisioning

Intermediate35 min to complete13 min read

Understand how Kubernetes storage works — from PersistentVolumes to StorageClasses — and provision storage dynamically for production workloads. Covers access modes, reclaim policies, volume expansion, and common mistakes.

Before you begin

  • kubectl installed and configured
  • Access to a Kubernetes cluster with a CSI driver (EKS
  • GKE
  • or kind with local-path-provisioner)
  • Basic familiarity with Kubernetes pods
Kubernetes
Storage
PVC
StorageClass
CSI
DevOps

Containers are ephemeral. Write a file inside a container and it disappears when the pod is deleted, rescheduled, or crashes. Kubernetes storage is the layer that survives pod lifecycle events and outlasts the containers that use it.

Understanding the storage model is a prerequisite to running any stateful workload — databases, message queues, file storage, or anything that needs data to persist. This tutorial walks through the three-level model (PV, PVC, StorageClass), shows dynamic provisioning in practice, and explains the choices you'll need to make for production workloads.

What You'll Build

A pod writing to a PersistentVolumeClaim, verified to survive a pod restart. Then a custom StorageClass so you understand what's happening when you request storage, rather than accepting defaults you don't understand.

Step 1: Understand the Three-Level Model

Pod → PVC → PV ← StorageClass (auto-provisions)
  • PersistentVolume (PV): A piece of storage in the cluster. Can be a cloud disk (EBS, GCE PD), NFS share, or local directory. Created by an admin manually, or automatically by a CSI driver when a PVC requests it.
  • PersistentVolumeClaim (PVC): A request for storage from a pod or StatefulSet. Specifies how much storage and what access mode. Kubernetes binds the PVC to a matching available PV.
  • StorageClass: A template that tells Kubernetes how to provision a PV on demand. Points to a CSI driver, sets the disk type, reclaim policy, and other options.

Before StorageClasses existed, admins manually pre-provisioned PVs. Dynamic provisioning via StorageClasses means the PV is created on demand, sized exactly as requested, when the PVC is applied.

Step 2: Create a PVC

Start by claiming 1Gi of storage:

bash
1kubectl apply -f - <<EOF
2apiVersion: v1
3kind: PersistentVolumeClaim
4metadata:
5  name: data-pvc
6spec:
7  accessModes:
8    - ReadWriteOnce
9  storageClassName: standard
10  resources:
11    requests:
12      storage: 1Gi
13EOF

Replace standard with the appropriate StorageClass for your cluster:

  • EKS: gp2 or gp3
  • GKE: standard or standard-rwo
  • kind: standard (uses rancher/local-path-provisioner)
  • OpenShift: managed-premium

Check the status:

bash
kubectl get pvc data-pvc
# NAME       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
# data-pvc   Bound    pvc-f3a9b4c2-...                           1Gi        RWO            standard       8s

Bound means a PV was found or provisioned to satisfy the claim. Pending means the PVC is waiting for a matching PV — either none exists, or the StorageClass is provisioning one (which takes a few seconds for cloud disks).

Step 3: Mount the PVC in a Pod

bash
1kubectl apply -f - <<EOF
2apiVersion: v1
3kind: Pod
4metadata:
5  name: writer
6spec:
7  volumes:
8    - name: data
9      persistentVolumeClaim:
10        claimName: data-pvc
11  containers:
12    - name: app
13      image: busybox:1.36
14      command: ["sh", "-c", "echo 'persistent data' > /data/test.txt && sleep 3600"]
15      volumeMounts:
16        - name: data
17          mountPath: /data
18EOF
bash
kubectl wait --for=condition=ready pod writer --timeout=60s
kubectl exec writer -- cat /data/test.txt
# persistent data

Step 4: Verify Persistence Across Pod Restart

Delete the pod and create a new one using the same PVC:

bash
kubectl delete pod writer
bash
1kubectl apply -f - <<EOF
2apiVersion: v1
3kind: Pod
4metadata:
5  name: reader
6spec:
7  volumes:
8    - name: data
9      persistentVolumeClaim:
10        claimName: data-pvc
11  containers:
12    - name: app
13      image: busybox:1.36
14      command: ["sleep", "3600"]
15      volumeMounts:
16        - name: data
17          mountPath: /data
18EOF
bash
kubectl wait --for=condition=ready pod reader --timeout=60s
kubectl exec reader -- cat /data/test.txt
# persistent data

The file survived. The PV is a separate object from the pod — deleting the pod only unmounts the volume, it doesn't delete it.

Step 5: Access Modes

Access modes define how many nodes can mount the volume simultaneously and in what mode:

ModeShortMeaningTypical storage
ReadWriteOnceRWOOne node, read-writeEBS, GCE PD, Azure Disk
ReadWriteManyRWXMultiple nodes, read-writeNFS, CephFS, EFS
ReadOnlyManyROXMultiple nodes, read-onlyPre-populated data volumes
ReadWriteOncePodRWOPOne pod, read-writeEBS (GA in Kubernetes 1.29; alpha 1.22–1.26, beta 1.27–1.28)

The most critical distinction: ReadWriteOnce means one node, not one pod. Two pods on the same node can both mount an RWO volume. But if you try to schedule a second pod on a different node that needs the same RWO volume, one of them will get stuck in a ContainerCreating state waiting for the volume to be detached from the first node.

For shared storage (e.g., multiple pods writing to the same upload directory), you need ReadWriteMany — which requires NFS or a distributed filesystem. EBS and GCE PD do not support RWX.

Step 6: Reclaim Policies

When a PVC is deleted, what happens to the PV (and the underlying disk)?

bash
kubectl describe storageclass standard | grep ReclaimPolicy
# ReclaimPolicy:  Delete

Three options:

  • Delete (default for dynamically provisioned PVs): The PV and underlying storage are deleted when the PVC is deleted. This is what you want for ephemeral dev environments. It is dangerous on production databases.
  • Retain: The PV survives PVC deletion. The disk is preserved and the PV enters Released state. You must manually reclaim it — either delete the PV and let it be reprovisioned, or patch it to remove the claimRef so it can be rebound.
  • Recycle (deprecated since Kubernetes 1.9): Do not use. Performs a basic rm -rf and makes the PV available for reuse. Use Delete or Retain instead.

For any StorageClass backing production databases, create it with reclaimPolicy: Retain:

bash
1kubectl apply -f - <<EOF
2apiVersion: storage.k8s.io/v1
3kind: StorageClass
4metadata:
5  name: fast-retain
6provisioner: ebs.csi.aws.com
7parameters:
8  type: gp3
9  encrypted: "true"
10reclaimPolicy: Retain
11allowVolumeExpansion: true
12volumeBindingMode: WaitForFirstConsumer
13EOF

Step 7: Volume Expansion

Once a PVC is bound, you can increase its size if the StorageClass has allowVolumeExpansion: true:

bash
kubectl patch pvc data-pvc -p '{"spec":{"resources":{"requests":{"storage":"5Gi"}}}}'
bash
kubectl describe pvc data-pvc | grep -A3 Conditions
# Conditions:
#   Type                      Status
#   FileSystemResizePending   True

The PVC transitions through FileSystemResizePending until the node resizes the filesystem. On some providers (EBS), the pod must be restarted for the resize to complete. On others (GCE PD with ext4 online resize), it happens while the pod is running.

You cannot shrink a PVC. Kubernetes will reject any attempt to reduce spec.resources.requests.storage.

Step 8: volumeBindingMode

Two binding modes affect when a PV is provisioned:

  • Immediate: The PV is provisioned as soon as the PVC is created, before any pod claims it. On multi-AZ clusters, this can provision the disk in zone A while the pod eventually schedules in zone B — and the mount fails.
  • WaitForFirstConsumer: The PV is provisioned only when a pod with a matching PVC is scheduled. The scheduler picks a node first, and the PV is created in the same zone. This is the correct mode for multi-AZ deployments.

Check which mode your default StorageClass uses:

bash
kubectl describe storageclass standard | grep VolumeBindingMode
# VolumeBindingMode:  WaitForFirstConsumer

If it's Immediate and you're on a multi-AZ cluster, create a new StorageClass with WaitForFirstConsumer and use it for stateful workloads.

Common Mistakes to Avoid

Requesting RWX on a StorageClass that only supports RWO — the PVC stays Pending indefinitely. Check the StorageClass provisioner documentation before requesting RWX.

Default Delete reclaim policy on production databases — someone runs kubectl delete pvc for cleanup and destroys the database. Create a dedicated StorageClass with reclaimPolicy: Retain for anything you can't afford to lose.

Not checking allowVolumeExpansionkubectl patch pvc will be rejected by the API server with a Forbidden error if the StorageClass does not have allowVolumeExpansion: true. The patch does not silently succeed — verify your StorageClass supports expansion before requesting a resize.

volumeBindingMode: Immediate on multi-AZ clusters — disk provisioned in us-east-1a, pod scheduled on a node in us-east-1b. The pod fails with Multi-Attach error for volume. Switch to WaitForFirstConsumer.

Forgetting the PVC keeps the PV alive — even after all pods using a PVC are deleted, the PVC object itself keeps the PV bound. The PV is only released when the PVC is deleted. In dev environments, leftover PVCs accumulate and eat into storage quotas.

Cleanup

bash
kubectl delete pod reader
kubectl delete pvc data-pvc
# With ReclaimPolicy=Delete, the PV is deleted automatically
# Verify:
kubectl get pv

What's Next

Official References

  • Persistent Volumes — full reference for PV lifecycle, access modes, reclaim policies, and phase transitions
  • Storage Classes — configuring StorageClasses for different provisioners and disk types
  • Expanding Persistent Volumes — requirements and process for online volume expansion
  • CSI Drivers — list of CSI drivers for cloud providers and storage systems

We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.

Struggling with this in production?

We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.