Storage Classes: Static vs Dynamic Provisioning
Video: Day 52 — StorageClasses & Dynamic Provisioning • Theme: stop hand-making PVs; let a provisioner carve volumes on demand.
Key terms
| Term | Meaning |
|---|---|
| StorageClass | A template that provisions PVs on demand |
| Provisioner | The CSI driver that creates the real volume |
reclaimPolicy | Fate of the PV when the PVC is deleted (Delete/Retain) |
volumeBindingMode | Immediate or WaitForFirstConsumer |
allowVolumeExpansion | Whether PVCs of this class can grow |
| Default class | The class used when a PVC names none |
| Static provisioning | Admin pre-creates PVs by hand |
| Dynamic provisioning | The class auto-creates a PV per PVC |
Problem & solution
Static provisioning means an admin must pre-create a PersistentVolume for every
claim — slow, error-prone, and impossible to predict sizes for. App teams should
not wait on humans to get a disk.
Solution: A StorageClass binds a provisioner (a CSI driver) to a set of parameters. When a PVC references the class, the provisioner dynamically creates a matching PV — the developer writes only the PVC.
The analogy
Instead of pre-building every storage unit and hoping the sizes fit, the port publishes warehouse tiers: a fast climate-controlled locker for sensitive cargo and a cheap bulk shed for everything else. A tenant just files a claim slip naming the tier and the size they need, and the port builds that exact unit on demand and hands over the key. In Kubernetes each tier is a StorageClass, the claim slip is a PersistentVolumeClaim, and the unit built to match is a dynamically provisioned PersistentVolume.
Where this fits in the cluster
The same cluster entities appear in every day's notes; the <== marks what this day touches.
Static vs dynamic, side by side
Here is the core difference at a glance, namely who creates the PersistentVolume and when.
STATIC DYNAMIC
------ -------
admin writes PV yaml by hand developer writes only the PVC
PVC binds to a matching pre-made PV StorageClass provisioner creates a PV
sizing guessed up front size taken from the PVC request
good for: existing NFS/iSCSI exports good for: cloud disks, self-service
Anatomy of a StorageClass
The class names the provisioner and the knobs the provisioner understands.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com # the CSI driver that makes the volume
parameters: # driver-specific knobs
type: gp3
encrypted: "true"
reclaimPolicy: Delete # Delete (default) or Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
kubectl get storageclass
kubectl describe sc fast-ssd
kubectl get sc # the (default) one is marked
reclaimPolicy: what happens to data on PVC delete
The class stamps its reclaimPolicy onto every PV it creates.
Delete -> deleting the PVC deletes the PV and the cloud disk (data gone)
Retain -> PV stays in 'Released'; admin reclaims the data manually
Dynamically provisioned PVs default to Delete. Use Retain for data you must not lose to an accidental
kubectl delete pvc.
volumeBindingMode: when binding happens
This setting decides whether the PV is created the instant the PVC appears or deferred until a pod actually needs it — which matters for topology.
Immediate -> provision the PV as soon as the PVC is created
(risk: PV lands in a zone the pod can't schedule to)
WaitForFirstConsumer -> wait until a pod using the PVC is scheduled,
then provision in the pod's zone/node
(recommended for zonal/topology-aware storage)
Dynamic provisioning: the developer writes only the PVC
With a class in place, the workflow is just a claim. The provisioner creates the PV; the PVC binds to it; the pod mounts the claim.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-claim
spec:
storageClassName: fast-ssd # name the class; "" disables dynamic
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: nginx
volumeMounts:
- mountPath: /data
name: data
volumes:
- name: data
persistentVolumeClaim:
claimName: data-claim
kubectl apply -f pvc.yaml
kubectl get pvc data-claim # Pending until consumer if WaitForFirstConsumer
kubectl apply -f pod.yaml
kubectl get pvc,pv # PV auto-created; STATUS Bound
The default StorageClass
A PVC with no storageClassName gets the cluster's default class (via the
DefaultStorageClass admission controller). Exactly one class should carry the
default annotation.
# mark a class default (and there must be only one default)
kubectl annotate sc fast-ssd storageclass.kubernetes.io/is-default-class="true"
# remove default from another class
kubectl annotate sc old-class storageclass.kubernetes.io/is-default-class="false" --overwrite
storageClassName: ""(empty string) explicitly opts a PVC out of dynamic provisioning, forcing it to bind a pre-made PV.
Expanding a volume
If the class sets allowVolumeExpansion: true, edit the PVC's request to grow
it; the CSI driver resizes the backing disk online (filesystem grow may need a
pod restart on some drivers).
kubectl patch pvc data-claim -p '{"spec":{"resources":{"requests":{"storage":"40Gi"}}}}'
kubectl get pvc data-claim # CAPACITY grows once resize completes
End-to-end: a PVC triggers dynamic provisioning
The full flow from claim to a mounted, bound volume in the right zone.
End-to-end example: dynamic provisioning then a live PVC expand
A complete walkthrough: create a WaitForFirstConsumer StorageClass, claim a
volume that stays Pending until a Pod needs it, mount it, write data, then grow
the PVC online.
Step 1 — create the StorageClass (defer binding, allow expansion).
# sc.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: topology-ssd
provisioner: ebs.csi.aws.com
parameters:
type: gp3
encrypted: "true"
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
kubectl apply -f sc.yaml
# storageclass.storage.k8s.io/topology-ssd created
kubectl get sc topology-ssd
# NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION
# topology-ssd ebs.csi.aws.com Delete WaitForFirstConsumer true
Step 2 — create the PVC; it stays Pending (no consumer yet).
# pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-claim
spec:
storageClassName: topology-ssd
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
kubectl apply -f pvc.yaml
kubectl get pvc data-claim
# NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
# data-claim Pending topology-ssd 5s
kubectl describe pvc data-claim | grep -A2 Events
# Normal WaitForFirstConsumer waiting for first consumer to be created before binding
Step 3 — schedule a Pod that mounts the claim; provisioning fires.
# pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: writer
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "echo hello > /data/marker && sleep 3600"]
volumeMounts:
- mountPath: /data
name: data
volumes:
- name: data
persistentVolumeClaim:
claimName: data-claim
kubectl apply -f pod.yaml
kubectl wait --for=condition=Ready pod/writer --timeout=120s
kubectl get pvc,pv
# NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS
# persistentvolumeclaim/data-claim Bound pvc-8a3f.. 10Gi RWO topology-ssd
# NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM
# persistentvolume/pvc-8a3f.. 10Gi RWO Delete Bound default/data-claim
Step 4 — verify the mount and that data persists.
kubectl exec writer -- cat /data/marker
# hello
kubectl exec writer -- df -h /data
# Filesystem Size Used Avail Use% Mounted on
# /dev/nvme1n1 9.8G 24K 9.8G 1% /data
Step 5 — expand the PVC online (allowVolumeExpansion: true).
kubectl patch pvc data-claim --type merge \
-p '{"spec":{"resources":{"requests":{"storage":"30Gi"}}}}'
# persistentvolumeclaim/data-claim patched
# the CSI driver resizes the backing disk, then the filesystem grows
kubectl get pvc data-claim -w
# NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS
# data-claim Bound pvc-8a3f.. 10Gi RWO topology-ssd
# data-claim Bound pvc-8a3f.. 30Gi RWO topology-ssd
kubectl exec writer -- df -h /data
# Filesystem Size Used Avail Use% Mounted on
# /dev/nvme1n1 29G 24K 29G 1% /data
Key takeaways
- A StorageClass = provisioner + parameters; it auto-creates PVs for PVCs.
- Static = admin hand-makes PVs; dynamic = the class provisions on demand.
reclaimPolicyDelete(default) destroys data on PVC delete; useRetainto keep it.volumeBindingMode: WaitForFirstConsumerprovisions in the pod's zone — use it for zonal disks.- One default class serves PVCs with no class;
""opts a PVC out of dynamic provisioning.
Checklist
- [ ] Listed StorageClasses and identified the default
- [ ] Created a PVC with no PV and watched a PV auto-provision
- [ ] Explained
DeletevsRetainreclaim policy - [ ] Compared
ImmediatevsWaitForFirstConsumerbinding - [ ] Expanded a PVC on a class with
allowVolumeExpansion