StorageClasses and Dynamic Provisioning¶
What You'll Learn¶
- What a StorageClass actually configures, and how it connects to a CSI driver
- The end-to-end dynamic provisioning flow, from
kubectl applyon a PVC to a bound, mountable volume - How
allowVolumeExpansionand default StorageClass behavior work, and where they commonly go wrong
Why This Matters¶
Hand-writing a PersistentVolume for every PVC doesn't scale past a handful of workloads, and it means a human is in the loop every time an application needs storage. StorageClasses turn "give me 50Gi of fast storage" into something a developer can request with a PVC alone — no admin ticket, no manually created PV. This is how storage is provisioned on essentially every modern cluster.
Mental Model¶
A StorageClass is a template: it names a provisioner (almost always a CSI driver today) and the parameters that provisioner needs (disk type, IOPS, filesystem, replication) to create a volume on demand. A PVC that references a StorageClass doesn't wait for a human to create a matching PV — the provisioner creates one automatically.
| Concept | Role |
|---|---|
| StorageClass | Declares which provisioner to use and how (disk type, IOPS, encryption, replication) |
| CSI driver | The plugin (usually a DaemonSet + Deployment pair) that actually talks to the storage backend's API to create/attach/resize volumes |
| Provisioner | The CSI driver name referenced by provisioner: in the StorageClass |
| PVC | The request that triggers the provisioner when no matching PV already exists |
How It Works¶
CSI has replaced in-tree provisioners¶
Older clusters used in-tree volume plugins — provisioner strings like kubernetes.io/aws-ebs or kubernetes.io/gce-pd compiled directly into kube-controller-manager. That model is legacy: in-tree plugins are deprecated (many removed outright since the 1.26+ line) in favor of the Container Storage Interface (CSI), an out-of-tree standard that lets any storage vendor ship a driver without touching Kubernetes core. Every current recommendation — AWS, GCP, Azure, and every serious on-prem storage vendor — is CSI-based. Treat kubernetes.io/* in-tree provisioners as legacy context only; new StorageClasses should target a CSI driver name like ebs.csi.aws.com.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
type: gp3
iops: "3000"
throughput: "125"
encrypted: "true"
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
The dynamic provisioning flow, end to end¶
sequenceDiagram
participant Dev as Developer
participant API as API Server
participant Ctrl as CSI External Provisioner
participant Cloud as Cloud Storage API
participant Kubelet as kubelet + CSI Node plugin
Dev->>API: Apply PVC (storageClassName: fast-ssd)
API->>Ctrl: Watches unbound PVCs
Ctrl->>Cloud: CreateVolume(size, parameters)
Cloud-->>Ctrl: Volume ID
Ctrl->>API: Create PV, bind to PVC
Dev->>API: Apply Pod referencing the PVC
API->>Kubelet: Schedule pod to a node
Kubelet->>Cloud: ControllerPublishVolume (attach to node)
Kubelet->>Kubelet: NodeStageVolume + NodePublishVolume (mount into pod)
- A PVC is created referencing a StorageClass by name (or the cluster's default StorageClass, if
storageClassNameis omitted). - The CSI external-provisioner sidecar (running alongside the CSI controller plugin) notices the unbound PVC and calls the driver's
CreateVolumegRPC method with the StorageClass'sparameters. - The storage backend creates the volume and returns an ID; the provisioner creates a PV object pointing at it and binds it to the PVC.
- When a pod referencing the PVC is scheduled, the CSI node plugin (running as a DaemonSet pod on that node) attaches and mounts the volume into the pod's filesystem.
volumeBindingMode: WaitForFirstConsumer (rather than the older Immediate) delays step 2 until a pod actually needs the volume, so the provisioner can create the disk in the same availability zone the scheduler picks for the pod — critical for zonal block storage that can't attach cross-zone.
Volume expansion¶
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi # was 20Gi — edit and re-apply to grow
storageClassName: fast-ssd
If the StorageClass has allowVolumeExpansion: true and the CSI driver supports resize, increasing resources.requests.storage on an existing PVC and re-applying it triggers an online (usually) expansion — no data loss, though the filesystem inside the pod may need a restart to see the new size depending on the driver and filesystem type. Expansion is one-directional: PVCs cannot be shrunk this way.
Default StorageClass behavior¶
Exactly one StorageClass in a cluster should carry the annotation storageclass.kubernetes.io/is-default-class: "true". Any PVC that omits storageClassName entirely is bound using that default. Two common failure modes:
- No default set — PVCs without an explicit
storageClassNamestayPendingforever with no clear error pointing at the cause. - Two StorageClasses marked default — behavior is undefined/version-dependent; the admission controller does not guarantee which one wins, so this should be treated as a misconfiguration to fix immediately, not relied upon.
kubectl get storageclass
kubectl patch storageclass standard \
-p '{"metadata": {"annotations": {"storageclass.kubernetes.io/is-default-class": "true"}}}'
Common Mistakes¶
- Writing a new StorageClass against a deprecated in-tree provisioner (
kubernetes.io/aws-ebs) instead of the CSI driver name (ebs.csi.aws.com) — in-tree support is being removed across supported Kubernetes versions. - Setting
volumeBindingMode: Immediatefor zonal block storage, which lets the PV get provisioned in a zone the scheduler then can't place the pod into. - Shrinking a PVC's
resources.requests.storageand expecting it to work — expansion is expand-only. - Marking more than one StorageClass as default, leaving PVC-to-class resolution ambiguous.
Interview Questions¶
- Why did Kubernetes move from in-tree volume plugins to CSI, and what does that mean for a StorageClass's
provisionerfield? - Walk through what happens between applying a PVC and a pod actually mounting the resulting disk.
- What problem does
volumeBindingMode: WaitForFirstConsumersolve thatImmediatedoesn't?
See Interview Prep for full answers.
Next¶
Continue to StatefulSet Storage Patterns to see how dynamic provisioning is used to give each replica of a stateful workload its own durable volume.