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."

PVC storageClassName: fast StorageClass: fast provisioner: ebs.csi.aws.com type: gp3, iops: 3000 reclaimPolicy: Delete CSI Driver Creates EBS vol PV auto-created

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

FieldPurposeImportant Values
provisionerCSI driver that creates the volumeebs.csi.aws.com, pd.csi.storage.gke.io, disk.csi.azure.com
parametersPassed to the provisioner (vendor-specific)Disk type, IOPS, encryption, zones
reclaimPolicyWhat happens to PV+disk when PVC is deletedDelete (default) or Retain
volumeBindingModeWhen to provision the volumeImmediate or WaitForFirstConsumer
allowVolumeExpansionCan PVCs be resized?true / false

2. volumeBindingMode — When to Provision

This is the most important field for production correctness:

ModeBehaviorRisk
ImmediateVolume provisioned as soon as PVC is createdVolume may be in wrong zone — Pod can't schedule
WaitForFirstConsumerVolume provisioned only when a Pod uses the PVCNone — volume created in the correct zone for the Pod

The Zone Problem with Immediate

Problem: volumeBindingMode: Immediate PVC created → PV in us-east-1a Scheduler places Pod on us-east-1b (different zone!) ❌ Pod stuck Pending Volume not in Pod's zone Solution: volumeBindingMode: WaitForFirstConsumer Scheduler picks node first → volume created in same zone as selected node
# 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)
Always use 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.
CKA exam: if a Pod is stuck Pending with "volume node affinity conflict", the likely cause is 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
Most production clusters have 2-3 StorageClasses: (1) Default — general-purpose SSD (gp3, pd-balanced). (2) High-IOPS — for databases (io2, pd-ssd). (3) Shared — NFS/EFS for RWX workloads. Set one as default so PVCs without explicit 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 SettingResult
storageClassName: gp3Uses the "gp3" StorageClass explicitly
storageClassName: "" (empty string)No dynamic provisioning — only binds to PVs with no class
Field omitted entirelyUses 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
Use 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

ConceptKey Point
StorageClassTemplate for dynamic provisioning — defines how volumes are created
provisionerCSI driver that creates the actual storage (EBS, PD, Azure Disk, NFS)
parametersVendor-specific settings (disk type, IOPS, encryption)
volumeBindingModeWaitForFirstConsumer = always correct for zone-bound storage
ImmediateVolume created on PVC creation — risk of zone mismatch
WaitForFirstConsumerVolume created when Pod is scheduled — zone matches Pod's node
Default classAnnotated with is-default-class: "true" — used when PVC omits class
storageClassName: ""Explicitly no class — static provisioning only
reclaimPolicyDelete (auto-cleanup) or Retain (preserve data after PVC deletion)
allowVolumeExpansionMust 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?

The PVC stays Pending indefinitely. It won't bind to any PV (even if a matching Available PV exists with a different class) because the storageClassName must match exactly. Either create the "fast" StorageClass or fix the PVC to reference an existing class.

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?

The Pod is stuck Pending with "volume node affinity conflict." The EBS volume is in us-east-1c but can only be attached to nodes in that zone. The scheduler can't place the Pod in us-east-1a (no volume there). Fix: use 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?

Set 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?

Checklist: (1) Is the CSI driver DaemonSet/Deployment running? (2) Does the PVC explicitly set 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?

To restrict which zones/regions volumes can be created in. Use cases: (1) Compliance — data must stay in a specific zone. (2) Cost — only provision in zones where you have reserved capacity. (3) Performance — colocate storage with specific hardware. Combined with WaitForFirstConsumer, it constrains both where the Pod runs AND where the volume is created.