StorageClasses are the mechanism that enables dynamic provisioning — creating storage on-demand without admin intervention. They define "tiers" of storage (fast SSD, cheap HDD, replicated, encrypted) that developers can request by name in their PVCs.
1. What a StorageClass Does
A StorageClass is a cluster-scoped template that tells Kubernetes: "When someone requests this class of storage, use this provisioner with these parameters to create the volume."
StorageClass Spec
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
annotations:
storageclass.kubernetes.io/is-default-class: "true" # Default SC
provisioner: ebs.csi.aws.com # Which CSI driver creates the volume
parameters: # Provisioner-specific settings
type: gp3
iops: "3000"
throughput: "125"
encrypted: "true"
fsType: ext4
reclaimPolicy: Delete # What happens when PVC is deleted
allowVolumeExpansion: true # Allow PVC resize
volumeBindingMode: WaitForFirstConsumer # When to provision
mountOptions: # Options passed to mount command
- discard
- noatime
Key Fields
| Field | Purpose | Important Values |
|---|---|---|
provisioner | CSI driver that creates the volume | ebs.csi.aws.com, pd.csi.storage.gke.io, disk.csi.azure.com |
parameters | Passed to the provisioner (vendor-specific) | Disk type, IOPS, encryption, zones |
reclaimPolicy | What happens to PV+disk when PVC is deleted | Delete (default) or Retain |
volumeBindingMode | When to provision the volume | Immediate or WaitForFirstConsumer |
allowVolumeExpansion | Can PVCs be resized? | true / false |
2. volumeBindingMode — When to Provision
This is the most important field for production correctness:
| Mode | Behavior | Risk |
|---|---|---|
Immediate | Volume provisioned as soon as PVC is created | Volume may be in wrong zone — Pod can't schedule |
WaitForFirstConsumer | Volume provisioned only when a Pod uses the PVC | None — volume created in the correct zone for the Pod |
The Zone Problem with Immediate
# With WaitForFirstConsumer: # 1. PVC created → stays Pending (no volume yet) # 2. Pod is created referencing the PVC # 3. Scheduler picks a node (considering all constraints) # 4. Volume is provisioned in the same zone as the chosen node # 5. PVC binds → Pod starts # This is why WaitForFirstConsumer is the CORRECT default for all # zone-aware storage (EBS, GCE PD, Azure Disk)
WaitForFirstConsumer for zone-bound storage. With Immediate, the volume might be provisioned in zone A, but the scheduler might place the Pod in zone B (due to resource constraints, affinity rules, etc.). The Pod will never start because block storage can't cross zones. WaitForFirstConsumer eliminates this entirely.
Immediate binding mode with a multi-zone cluster. The fix: recreate the StorageClass with WaitForFirstConsumer, delete and recreate the PVC (you can't change binding mode on an existing PVC).
3. StorageClass Examples by Cloud
AWS (EBS CSI Driver)
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: gp3-encrypted provisioner: ebs.csi.aws.com parameters: type: gp3 # gp3, gp2, io2, io1, st1, sc1 iops: "3000" # Baseline IOPS (gp3: up to 16,000) throughput: "125" # MB/s (gp3: up to 1,000) encrypted: "true" kmsKeyId: arn:aws:kms:... # Optional: customer-managed key volumeBindingMode: WaitForFirstConsumer reclaimPolicy: Delete allowVolumeExpansion: true
GCP (PD CSI Driver)
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: ssd-balanced provisioner: pd.csi.storage.gke.io parameters: type: pd-balanced # pd-standard, pd-balanced, pd-ssd, pd-extreme volumeBindingMode: WaitForFirstConsumer reclaimPolicy: Delete allowVolumeExpansion: true
Azure (Disk CSI Driver)
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: premium-ssd provisioner: disk.csi.azure.com parameters: skuName: Premium_LRS # Premium_LRS, StandardSSD_LRS, Standard_LRS cachingMode: ReadOnly volumeBindingMode: WaitForFirstConsumer reclaimPolicy: Delete allowVolumeExpansion: true
Local Path (for development — Rancher Local Path Provisioner)
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: local-path provisioner: rancher.io/local-path volumeBindingMode: WaitForFirstConsumer reclaimPolicy: Delete
NFS (for ReadWriteMany)
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-shared provisioner: nfs.csi.k8s.io parameters: server: nfs-server.internal share: /exports/k8s reclaimPolicy: Retain volumeBindingMode: Immediate # NFS is zone-agnostic mountOptions: - nfsvers=4.1 - hard
storageClassName get provisioned automatically.
4. Default StorageClass
If a PVC doesn't specify storageClassName, Kubernetes uses the default StorageClass:
# Mark a StorageClass as default: kubectl annotate storageclass gp3-encrypted \ storageclass.kubernetes.io/is-default-class=true # Check which is default: kubectl get sc # NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE AGE # gp3-encrypted (default) ebs.csi.aws.com Delete WaitForFirstConsumer 30d # premium-iops ebs.csi.aws.com Retain WaitForFirstConsumer 30d # nfs-shared nfs.csi.k8s.io Retain Immediate 30d
PVC storageClassName Behavior
| PVC Setting | Result |
|---|---|
storageClassName: gp3 | Uses the "gp3" StorageClass explicitly |
storageClassName: "" (empty string) | No dynamic provisioning — only binds to PVs with no class |
| Field omitted entirely | Uses the default StorageClass (if one exists) |
storageClassName: "" ≠ omitted. An empty string explicitly means "no class" (disables dynamic provisioning, only static PVs match). Omitting the field means "use the default class." This distinction trips people up — if you want static provisioning, explicitly set "".
5. Practical Patterns
Pattern: Different Tiers for Different Workloads
# In the same cluster:
# PVC for a database (high IOPS):
spec:
storageClassName: premium-iops
resources:
requests:
storage: 100Gi
# PVC for log storage (cheap, large):
spec:
storageClassName: standard-hdd
resources:
requests:
storage: 500Gi
# PVC for shared config (NFS, RWX):
spec:
storageClassName: nfs-shared
accessModes: ["ReadWriteMany"]
resources:
requests:
storage: 1Gi
Pattern: Preventing Accidental Deletion (Override Reclaim Policy)
# StorageClass with Delete (for general use):
reclaimPolicy: Delete
# For critical databases, OVERRIDE on the PV after provisioning:
kubectl patch pv pvc-abc123 -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
# Now even if PVC is deleted, the EBS volume survives
Pattern: Topology-Restricted Provisioning
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: zone-a-only
provisioner: ebs.csi.aws.com
parameters:
type: gp3
allowedTopologies: # Only provision in specific zones
- matchLabelExpressions:
- key: topology.kubernetes.io/zone
values:
- us-east-1a
volumeBindingMode: WaitForFirstConsumer
allowedTopologies when you need volumes in specific zones (compliance, data locality). Combined with WaitForFirstConsumer, the scheduler ensures the Pod lands in the same zone. Without allowedTopologies, WaitForFirstConsumer provisions in whatever zone the scheduler picks for the Pod.
Summary
| Concept | Key Point |
|---|---|
| StorageClass | Template for dynamic provisioning — defines how volumes are created |
| provisioner | CSI driver that creates the actual storage (EBS, PD, Azure Disk, NFS) |
| parameters | Vendor-specific settings (disk type, IOPS, encryption) |
| volumeBindingMode | WaitForFirstConsumer = always correct for zone-bound storage |
| Immediate | Volume created on PVC creation — risk of zone mismatch |
| WaitForFirstConsumer | Volume created when Pod is scheduled — zone matches Pod's node |
| Default class | Annotated with is-default-class: "true" — used when PVC omits class |
storageClassName: "" | Explicitly no class — static provisioning only |
| reclaimPolicy | Delete (auto-cleanup) or Retain (preserve data after PVC deletion) |
| allowVolumeExpansion | Must be true for PVC resize to work |
📝 Quiz: StorageClasses & Dynamic Provisioning
Q1: A PVC references storageClassName: fast but no StorageClass named "fast" exists. What happens?
Q2: Your cluster is multi-zone (us-east-1a, 1b, 1c). A StorageClass uses volumeBindingMode: Immediate. A PVC is created. The provisioner creates a volume in us-east-1c. Then a Pod using this PVC needs to schedule, but only us-east-1a has resources. What happens?
WaitForFirstConsumer — the volume would have been created in whatever zone the scheduler picks.Q3: What's the difference between a PVC with storageClassName: "" (empty string) and one that omits the field entirely?
storageClassName: "": No dynamic provisioning. Only binds to PVs that also have no storageClassName set. Used for static provisioning.Field omitted: Uses the default StorageClass (annotated with
is-default-class: "true"). Dynamic provisioning happens normally via the default class.Q4: You want PVCs to be dynamically provisioned but data to survive PVC deletion. How do you configure this?
reclaimPolicy: Retain on the StorageClass. When a PVC is deleted, the dynamically provisioned PV moves to Released state (not deleted), and the underlying disk is preserved. Alternatively, use Delete on the class but patch critical PVs to Retain after creation.Q5: A developer creates a PVC but it stays Pending even though a default StorageClass exists with a working provisioner. What should you check?
storageClassName: "" (bypasses default)? (3) With WaitForFirstConsumer, the PVC stays Pending until a Pod uses it — is there a Pod referencing it? (4) Check events: kubectl describe pvc — provisioner errors appear here. (5) Are there quota limits blocking PVC creation?Q6: Why would you use allowedTopologies on a StorageClass?
WaitForFirstConsumer, it constrains both where the Pod runs AND where the volume is created.