Kubernetes Persistent Volume Claims: Dynamic Provisioning That Won't Fail at 3 AM
Dynamic provisioning in Kubernetes: avoid PVC failures with StorageClass, reclaim policies, and production patterns that survive node failures and cloud outages..
20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.
- ✓Basic Kubernetes pod and deployment YAML syntax
- ✓Familiarity with kubectl commands
- ✓Understanding of persistent volumes (PV) and persistent volume claims (PVC) concepts
Dynamic provisioning creates PVs on demand via a StorageClass. Define a StorageClass with a provisioner (e.g., kubernetes.io/aws-ebs), then submit a PVC referencing that StorageClass. Kubernetes calls the provisioner to create the actual storage volume and binds it to the PVC automatically.
Imagine you run a co-working space. Instead of building desks in advance (static provisioning), you have a magic desk factory (StorageClass). When someone asks for a desk (PVC), the factory instantly builds one to their specs (size, speed) and delivers it. If they leave, the factory can either reclaim the desk or recycle it, depending on your policy.
You've seen it: a pod stuck in Pending for hours because someone forgot to pre-create a PV. Or worse, a statefulset that silently loses data when a node dies because the volume was on local SSD with no replication. Static provisioning is a ticking time bomb. Dynamic provisioning is the fix — but only if you understand the trade-offs. By the end of this, you'll design StorageClasses that survive node failures, cloud outages, and your own mistakes.
Why Static Provisioning Is a Production Liability
Before dynamic provisioning, you had to create PVs manually — one per claim. That meant either over-provisioning (wasting cloud spend) or under-provisioning (blocking deployments). In a CI/CD pipeline, every new environment required a human to run kubectl create pv or a script that often failed because the storage backend was out of quota. Dynamic provisioning solves this by making the cluster request storage on demand, just like pods request CPU. The key enabler is the StorageClass, which tells Kubernetes which provisioner to call and what parameters to pass (e.g., disk type, IOPS, replication).
kubernetes.io/aws-ebs (in-tree) instead of ebs.csi.aws.com (CSI) will work on older clusters but is deprecated. CSI drivers offer better features like volume expansion and snapshots. Always use the CSI driver if available.Designing a StorageClass That Matches Your Workload
Not all storage is equal. A MySQL database needs high IOPS and low latency. A log aggregator needs cheap, large capacity. A shared filesystem needs ReadWriteMany. Your StorageClass should encode these requirements. The parameters field is provisioner-specific — read the docs. For AWS EBS, you can set type (gp3, io1, st1), iops, throughput, and encrypted. For GCE PD, you set type (pd-ssd, pd-standard) and replication-type. For NFS, you set server and path. The volumeBindingMode matters: Immediate creates the PV as soon as the PVC is submitted, even if no pod needs it yet. WaitForFirstConsumer delays creation until a pod is scheduled, which respects topology constraints (e.g., zone affinity). Use WaitForFirstConsumer for stateful workloads to avoid orphaned volumes in wrong zones.
WaitForFirstConsumerImmediate bindingReclaim Policies: Delete vs Retain — Choose Wisely
The reclaimPolicy determines what happens to the PV and underlying storage when the PVC is deleted. Delete removes both — your cloud bill stays clean. Retain leaves the PV in Released state and the storage volume intact. You must manually delete the PV and then the cloud resource. Why would you ever use Retain? When the data is irreplaceable — a production database that must survive accidental PVC deletion. But Retain means you're on the hook for cleanup. I've seen teams burn thousands of dollars on orphaned EBS volumes because they forgot to clean up Retain PVs. My rule: use Delete for ephemeral environments and stateless apps. Use Retain only for stateful workloads with a backup strategy. And always set a StorageClass with Delete as default.
storageclass.kubernetes.io/is-default-class: "true". Then any PVC without storageClassName uses it. This prevents accidental use of the cluster's built-in (often slow) default.Volume Expansion: Growing Without Downtime
You will run out of disk space. It's not if, but when. Dynamic provisioning supports volume expansion if the StorageClass has allowVolumeExpansion: true and the CSI driver supports it. To expand, edit the PVC's spec.resources.requests.storage to a larger value. Kubernetes will call the provisioner to resize the underlying volume. The pod must be using the volume — if the filesystem supports online resize (most do), the new space is available immediately. If not, you may need to restart the pod. Always test expansion in staging. I've seen a resized volume fail because the underlying storage backend had a quota limit. Monitor kubectl describe pvc for events like Waiting for user to (re-)start a pod to finish file system resize of the volume.
Topology Constraints: Keeping Volumes Close to Pods
Cloud storage volumes are zonal — an EBS volume in us-east-1a cannot be attached to a pod in us-east-1b. If your pod gets scheduled in a different zone, the volume will fail to attach. The fix is volumeBindingMode: WaitForFirstConsumer combined with allowedTopologies in the StorageClass. This tells Kubernetes to delay PV creation until a pod is scheduled, then create the PV in the same zone as the pod. Without this, you risk Multi-Attach errors or scheduling failures. For multi-zone clusters, always use WaitForFirstConsumer for stateful workloads. For stateless workloads, use Immediate and accept that the PV may be in a different zone — but then your pod must be scheduled in that zone (use nodeSelector or affinity).
Immediate creates PV on PVC creation, ignoring pod topology. WaitForFirstConsumer defers PV creation until a pod using the PVC is scheduled, then creates the PV in the same zone. Use WaitForFirstConsumer for stateful workloads in multi-zone clusters to avoid cross-zone attachment failures.When Dynamic Provisioning Is Overkill
Dynamic provisioning adds complexity: you need a CSI driver, IAM permissions, and StorageClass definitions. For a single-node cluster (e.g., Minikube, Docker Desktop), static provisioning with hostPath is simpler and faster. For development environments where data loss is acceptable, use emptyDir instead of PVCs. For production, dynamic provisioning is almost always the right choice — but don't use it for temporary scratch space. Also, if your storage backend doesn't support dynamic provisioning (e.g., some on-premise NFS servers), you're stuck with static PVs. In that case, use a manual PV with a nodeSelector to pin pods to the node with the NFS mount.
nfs.csi.k8s.io) for dynamic provisioning. It creates subdirectories per PVC automatically, avoiding manual PV management.Common Mistakes That Burn Production
I've seen these mistakes repeatedly. First: forgetting to set storageClassName in the PVC, which uses the cluster's default StorageClass — often the slow, generic one. Always specify it explicitly. Second: using ReadWriteOnce for a deployment with multiple replicas — only one pod can write at a time, others will fail. Use ReadWriteMany only if the backend supports it (NFS, EFS, Azure Files). Third: not setting volumeMode: Filesystem (the default) when you need a raw block device. For databases like Cassandra, raw block mode gives better performance. Fourth: ignoring volumeBindingMode — using Immediate in a multi-zone cluster causes cross-zone attachment failures. Fifth: not monitoring PVC capacity — a full volume causes pod evictions. Set up alerts on kubelet_volume_stats_used_bytes.
The 4GB Container That Kept Dying
OutOfMemoryKilled every 2-3 hours, but memory limits were set to 8GB.volumeSizeLimit in the StorageClass to prevent unbounded growth. Also added log rotation in the app.- Always monitor disk usage on PVCs — a full volume kills pods faster than a memory leak.
Pending with event Failed to provision volume with StorageClass "standard": rpc error: code = InvalidArgument desc = volume type not supportedkubectl describe storageclass standard to check provisioner. 2. Verify the CSI driver is installed: kubectl get pods -n kube-system | grep csi. 3. Check cloud provider console for service limits. 4. Fix provisioner name or install missing CSI driver.FailedAttachVolume: Multi-Attach error for volume "pvc-xxx"kubectl get pvc. 2. Ensure pods using the same PVC are scheduled on the same node (use nodeSelector or pod affinity). 3. If using ReadWriteMany, verify backend supports it (e.g., NFS, EFS). 4. Delete extra pods and reschedule.Releasedkubectl get pv pvc-xxx -o yaml | grep reclaimPolicy. 2. If Retain, manually delete PV: kubectl delete pv pvc-xxx. 3. Delete cloud resource via console/CLI. 4. Set reclaimPolicy to Delete in StorageClass.kubectl describe storageclass <name> | grep Provisionerkubectl get pods -n kube-system | grep csi| File | Command / Code | Purpose |
|---|---|---|
| storageclass-aws-ebs.yaml | apiVersion: storage.k8s.io/v1 | Why Static Provisioning Is a Production Liability |
| pvc-mysql.yaml | apiVersion: v1 | Designing a StorageClass That Matches Your Workload |
| storageclass-retain.yaml | apiVersion: storage.k8s.io/v1 | Reclaim Policies: Delete vs Retain |
| expand-pvc.sh | kubectl patch pvc mysql-data -p '{"spec": {"resources": {"requests": {"storage":... | Volume Expansion |
| storageclass-topology.yaml | apiVersion: storage.k8s.io/v1 | Topology Constraints |
| static-pv-nfs.yaml | apiVersion: v1 | When Dynamic Provisioning Is Overkill |
| pvc-raw-block.yaml | apiVersion: v1 | Common Mistakes That Burn Production |
Key takeaways
for ephemeral data and Retain` only when you have a cleanup process — otherwise you'll burn cloud budget.Interview Questions on This Topic
How does `volumeBindingMode: WaitForFirstConsumer` affect PV creation timing and what problem does it solve?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.
That's Kubernetes. Mark it forged?
3 min read · try the examples if you haven't