Storage

StorageClasses & Dynamic Provisioning

● Intermediate ⏱ 12 min read

Static provisioning requires a cluster admin to pre-create a PersistentVolume before any developer can claim storage. For clusters with dozens of teams and hundreds of services, that doesn't scale. StorageClasses automate this: a developer submits a PVC, Kubernetes calls the provisioner specified in the StorageClass, a real cloud disk (or other storage) is created, a PV is registered, and the PVC is bound — all without admin intervention.

What Is a StorageClass?

A StorageClass is a cluster-scoped resource that defines a "class" of storage with specific characteristics: which provisioner creates the volumes, what parameters it uses, how fast the disks are, what the reclaim policy is. Developers reference a StorageClass by name in their PVC spec — they don't need to know how the storage is implemented.

Dynamic provisioning flow
1
developer creates PVC: storageClassName: fast-ssd, 50Gi, RWO
2
control plane looks up StorageClass fast-ssd, finds provisioner
3
provisioner calls cloud API → creates 50 GiB SSD disk
4
PV created automatically → PVC transitions to Bound → pod can start
No admin action required after StorageClass is set up once
Dynamic provisioning: the StorageClass provisioner creates real storage on-demand when a PVC is submitted.

Provisioner

The provisioner field identifies which plugin creates volumes. It's either a built-in provisioner or a CSI (Container Storage Interface) driver deployed in the cluster.

storageclass-basic.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: ebs.csi.aws.com   # CSI driver for AWS EBS
parameters:
  type: gp3
  iops: "3000"
  throughput: "125"
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

Common provisioners:

Cloud / backendProvisioner name
AWS EBSebs.csi.aws.com
GCP Persistent Diskpd.csi.storage.gke.io
Azure Diskdisk.csi.azure.com
Azure Filesfile.csi.azure.com
Local (on-prem)kubernetes.io/no-provisioner
NFS (CSI driver)nfs.csi.k8s.io
Longhorndriver.longhorn.io
Ceph / Rookrook-ceph.rbd.csi.ceph.com

Parameters

The parameters block is provisioner-specific. Kubernetes passes these key-value pairs directly to the CSI driver when creating a volume. Common AWS EBS parameters:

# AWS EBS (ebs.csi.aws.com)
parameters:
  type: gp3          # gp2, gp3, io1, io2, sc1, st1
  iops: "3000"       # gp3/io1/io2 only
  throughput: "125"  # gp3 only, MiB/s
  encrypted: "true"
  kmsKeyId: arn:aws:kms:...   # optional: customer-managed key

# GCP Persistent Disk (pd.csi.storage.gke.io)
parameters:
  type: pd-ssd       # pd-standard, pd-ssd, pd-balanced, pd-extreme

# Azure Disk (disk.csi.azure.com)
parameters:
  skuName: Premium_LRS   # Standard_LRS, Premium_LRS, UltraSSD_LRS

reclaimPolicy

When a dynamically provisioned PV's PVC is deleted, the reclaimPolicy on the StorageClass determines what happens to the underlying disk:

⚠️
Default reclaimPolicy is Delete on most managed clusters

EKS, GKE, and AKS all default to Delete. Accidentally deleting a PVC on a database StatefulSet in production will also delete the disk — and the data. Explicitly create a StorageClass with reclaimPolicy: Retain for any persistent data you care about, and reference it in your StatefulSet's volumeClaimTemplates.

volumeBindingMode

Controls when a PVC gets bound to a PV:

ModeWhen boundUse case
ImmediateAs soon as the PVC is createdStorage that doesn't depend on node locality (NFS, cloud file systems)
WaitForFirstConsumerWhen a pod using the PVC is scheduledBlock storage (EBS, PD, Azure Disk) — disk must be in the same AZ as the node

WaitForFirstConsumer is essential for zonal block storage. With Immediate, the EBS volume might be created in us-east-1a but the pod gets scheduled to us-east-1b — the pod fails to start because EBS volumes can't cross AZ boundaries.

Default StorageClass

A StorageClass can be marked as the cluster default — PVCs that omit storageClassName automatically use it. Only one StorageClass should be marked default at a time.

# Mark a StorageClass as default
metadata:
  name: standard
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"

# Check which StorageClass is the default (look for "(default)" in the NAME column)
kubectl get storageclass
# NAME                PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE
# standard (default)  ebs.csi.aws.com         Delete          WaitForFirstConsumer

Cloud Provider Examples

cloud-storage-classes.yaml
# AWS — general purpose SSD, AZ-aware
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: aws-gp3
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
---
# GCP — SSD persistent disk
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: gcp-ssd
provisioner: pd.csi.storage.gke.io
parameters:
  type: pd-ssd
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
---
# Azure — premium managed disk
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: azure-premium
provisioner: disk.csi.azure.com
parameters:
  skuName: Premium_LRS
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

Local StorageClass

For on-premises clusters or when you need NVMe performance with no network overhead, use a local StorageClass. Local volumes bind a PVC to a specific node — pods using them must be scheduled to that node.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-nvme
provisioner: kubernetes.io/no-provisioner   # no dynamic provisioning
volumeBindingMode: WaitForFirstConsumer     # required for local volumes
reclaimPolicy: Retain

Local volumes still require manually creating the PV (pointing at a directory on a specific node). Tools like the local-static-provisioner automate PV creation for local disks.

Volume Expansion

When allowVolumeExpansion: true is set on a StorageClass, you can grow a PVC after creation by editing its spec.resources.requests.storage. Shrinking is not supported.

# Expand a PVC from 50Gi to 100Gi
kubectl patch pvc postgres-data -n production \
  -p '{"spec":{"resources":{"requests":{"storage":"100Gi"}}}}'

# Check expansion status
kubectl describe pvc postgres-data -n production
# Look for: "Normal  ExternalExpanding  Waiting for an external controller to expand this PVC"
# Then:     "Normal  Resizing           External resizer is resizing volume"
# Finally:  "Normal  FileSystemResizeRequired  Require file system resize"
# (file system resize happens automatically when the pod restarts)

kubectl Commands

# List all StorageClasses
kubectl get storageclass
kubectl get sc    # shorthand

# Describe a StorageClass (shows provisioner, parameters, binding mode)
kubectl describe storageclass fast-ssd

# Create a StorageClass
kubectl apply -f storageclass.yaml

# Change the default StorageClass (remove annotation from old, add to new)
kubectl annotate sc old-default storageclass.kubernetes.io/is-default-class-
kubectl annotate sc new-default storageclass.kubernetes.io/is-default-class=true

# List PVCs and their StorageClass
kubectl get pvc -A -o wide

# Check what StorageClass a PV was created with
kubectl get pv -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.storageClassName}{"\n"}{end}'