StorageClasses & provisioning
Dynamic provisioning and binding modes.
Hand-cutting a disk for every app that asks for one does not scale. Ten teams, forty databases, three availability zones, and you turn into the person clicking 'create volume' at 2am. A StorageClass ends that. Think of it as a standing order the cluster keeps on file: it says which storage system to use and with what settings, and the cluster fills the order the moment an app asks. The app asks with a PersistentVolumeClaim, or PVC. That's a pod's written request for storage, and a pod is the smallest thing Kubernetes runs, one or more containers bundled together. Out comes a PersistentVolume, or PV, which is the real disk, created and attached with nobody in the loop. That auto-fill is called dynamic provisioning, and it is why modern clusters almost never keep a shelf of pre-built disks sitting around waiting to be claimed.
The order form, not the disk
A StorageClass holds no storage of its own. It is a short recipe, and a handful of fields carry all the weight. provisioner names the plugin that does the real work, almost always a CSI driver. CSI is the Container Storage Interface, the standard plug that lets any storage vendor snap into Kubernetes the way a wall socket accepts any brand of appliance. parameters get handed straight to that driver: disk type, IOPS (how many reads and writes per second the disk allows), whether to encrypt the data at rest. reclaimPolicy decides the disk's fate when its claim is deleted, either Delete to tear it down or Retain to keep it around for recovery. allowVolumeExpansion lets you grow a volume later. And volumeBindingMode sets when provisioning happens, which turns out to be the field that quietly bites people. One class usually wears the annotation storageclass.kubernetes.io/is-default-class set to true, and any PVC that names no class inherits it.
apiVersion: storage.k8s.io/v1kind: StorageClassmetadata:name: fastannotations:storageclass.kubernetes.io/is-default-class: "true"provisioner: ebs.csi.aws.comparameters:type: gp3encrypted: "true"reclaimPolicy: DeleteallowVolumeExpansion: truevolumeBindingMode: WaitForFirstConsumer
$ kubectl apply -f storageclass.yaml$ kubectl get storageclass
storageclass.storage.k8s.io/fast createdNAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGEfast (default) ebs.csi.aws.com Delete WaitForFirstConsumer true 12dstandard ebs.csi.aws.com Retain Immediate true 12d
What fires when a claim shows up
A mailroom clerk has one job: watch the incoming tray. A parcel lands, they read the label, fetch the item, set it on the outgoing shelf. Kubernetes runs that exact loop for storage. A CSI driver ships a small helper container, a sidecar, called the external-provisioner, and it watches every PVC in the cluster. When a claim shows up naming a class whose provisioner matches this driver, the sidecar calls CreateVolume on the driver, the driver calls out to the cloud provider to actually cut the disk, and then the sidecar writes a PV object back into Kubernetes and binds your claim to it. From where you sit, you only ever created a PVC. Everything after that was a controller reacting to it. That is the whole model of Kubernetes storage in one sentence: you declare what you want, a controller notices, and it makes reality match.
Here is the part that trips up everyone the first time. On the recommended binding mode, the clerk does not move the instant your claim lands. Apply a PVC against a class set to WaitForFirstConsumer and it just sits there, Pending, apparently doing nothing at all.
$ kubectl apply -f pvc.yaml$ kubectl -n payments get pvc payments-data
persistentvolumeclaim/payments-data createdNAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGEpayments-data Pending fast 9s
Binding modes and the zone trap
Two settings, one real consequence. Immediate provisions the disk the second the PVC is created. On a single-zone cluster that does no harm. On a cluster spread across three availability zones it is a landmine. The driver cuts the disk in whatever zone it likes, before anyone knows where the pod will run. The scheduler later places the pod in a different zone. And a cloud block disk cannot cross a zone boundary, the same way a filing cabinet bolted to one floor of a building can't be read from another. The pod gets stuck, the claim shows Bound (which fools you into thinking storage is fine), and the pod's event reads 'volume node affinity conflict.' WaitForFirstConsumer flips the order. It holds off until a pod that uses the claim is actually being scheduled, lets the scheduler pick the node first, then provisions the disk in that node's zone. Same zone by construction. For any zonal block storage, whether that's EBS (Amazon's block storage), Google Persistent Disk, or Azure Disk, this is the mode you want, and it is exactly why the Pending claim above was healthy. It was waiting for its pod.
Once you wire a real pod to that claim, the whole story shows up in the claim's events. This is the first place to look whenever provisioning misbehaves, before you start blaming the storage backend. Read it top to bottom. WaitForFirstConsumer means it was waiting for a pod. Provisioning means the driver got the call. And ProvisioningSucceeded next to a Bound status means the disk exists and is attached. If you see nothing at all in the events, that usually points at the driver itself not running, not at your claim.
$ kubectl -n payments describe pvc payments-data
Name: payments-dataNamespace: paymentsStorageClass: fastStatus: BoundVolume: pvc-6d2f8b1a-4c07-4f2e-9a1b-8f3c2d5e7a90Access Modes: RWOEvents:Type Reason Age From Message---- ------ ---- ---- -------Normal WaitForFirstConsumer 62s persistentvolume-controller waiting for first consumer to be created before bindingNormal Provisioning 14s ebs.csi.aws.com_ebs-csi-controller-7d9 External provisioner is provisioning volume for claim "payments/payments-data"Normal ProvisioningSucceeded 11s ebs.csi.aws.com_ebs-csi-controller-7d9 Successfully provisioned volume pvc-6d2f8b1a-4c07-4f2e-9a1b-8f3c2d5e7a90
Growing a volume in place
Ran out of disk? If the class allows it, you grow a volume by editing the claim's requested size. No migration, no second PVC, no downtime on the modern drivers. Most CSI drivers resize the block device online while the pod keeps running. A few older ones need the pod to cycle so the filesystem can catch up, but the current EBS and Persistent Disk drivers do it live. One rule holds everywhere: you can only ever grow. Shrinking a PVC is not supported, and the API server rejects the edit outright.
$ kubectl -n payments patch pvc payments-data --type merge \-p '{"spec":{"resources":{"requests":{"storage":"50Gi"}}}}'$ kubectl -n payments get pvc payments-data
persistentvolumeclaim/payments-data patchedNAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGEpayments-data Bound pvc-6d2f8b1a-4c07-4f2e-9a1b-8f3c2d5e7a90 50Gi RWO fast 41m
WaitForFirstConsumer delays binding until a pod is scheduled — critical for zone-aware disks.
Default StorageClass mistakes send data to the slow or expensive tier. Be explicit in production YAML.
ReclaimPolicy on the class influences what happens when claims go away. Set it deliberately.
Try this
List StorageClasses, note the default, create a PVC without an explicit volumeName, and watch dynamic provisioning create a PV.
$ kubectl apply -f storageclass.yaml$ kubectl get storageclass$ kubectl apply -f pvc.yaml$ kubectl -n payments get pvc payments-data$ kubectl -n payments describe pvc payments-data$ kubectl -n payments patch pvc payments-data --type merge \-p '{"spec":{"resources":{"requests":{"storage":"50Gi"}}}}'$ kubectl -n payments get pvc payments-data
Takeaway
StorageClasses name provisioners and parameters. Dynamic provisioning turns a PVC into a real volume without pre-creating PVs.