52

Storage Classes: Static vs Dynamic Provisioning

Video: Day 52 — StorageClasses & Dynamic Provisioning • Theme: stop hand-making PVs; let a provisioner carve volumes on demand.

Key terms

TermMeaning
StorageClassA template that provisions PVs on demand
ProvisionerThe CSI driver that creates the real volume
reclaimPolicyFate of the PV when the PVC is deleted (Delete/Retain)
volumeBindingModeImmediate or WaitForFirstConsumer
allowVolumeExpansionWhether PVCs of this class can grow
Default classThe class used when a PVC names none
Static provisioningAdmin pre-creates PVs by hand
Dynamic provisioningThe class auto-creates a PV per PVC

Problem & solution

Static provisioning means an admin must pre-create a PersistentVolume for every claim — slow, error-prone, and impossible to predict sizes for. App teams should not wait on humans to get a disk.

Solution: A StorageClass binds a provisioner (a CSI driver) to a set of parameters. When a PVC references the class, the provisioner dynamically creates a matching PV — the developer writes only the PVC.

The analogy

Instead of pre-building every storage unit and hoping the sizes fit, the port publishes warehouse tiers: a fast climate-controlled locker for sensitive cargo and a cheap bulk shed for everything else. A tenant just files a claim slip naming the tier and the size they need, and the port builds that exact unit on demand and hands over the key. In Kubernetes each tier is a StorageClass, the claim slip is a PersistentVolumeClaim, and the unit built to match is a dynamically provisioned PersistentVolume.

Where this fits in the cluster

The same cluster entities appear in every day's notes; the <== marks what this day touches.

Static vs dynamic, side by side

Here is the core difference at a glance, namely who creates the PersistentVolume and when.

   STATIC                                  DYNAMIC
   ------                                  -------
   admin writes PV yaml by hand            developer writes only the PVC
   PVC binds to a matching pre-made PV     StorageClass provisioner creates a PV
   sizing guessed up front                 size taken from the PVC request
   good for: existing NFS/iSCSI exports    good for: cloud disks, self-service

Anatomy of a StorageClass

The class names the provisioner and the knobs the provisioner understands.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: ebs.csi.aws.com          # the CSI driver that makes the volume
parameters:                            # driver-specific knobs
  type: gp3
  encrypted: "true"
reclaimPolicy: Delete                  # Delete (default) or Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
kubectl get storageclass
kubectl describe sc fast-ssd
kubectl get sc                          # the (default) one is marked

reclaimPolicy: what happens to data on PVC delete

The class stamps its reclaimPolicy onto every PV it creates.

   Delete  -> deleting the PVC deletes the PV and the cloud disk (data gone)
   Retain  -> PV stays in 'Released'; admin reclaims the data manually

Dynamically provisioned PVs default to Delete. Use Retain for data you must not lose to an accidental kubectl delete pvc.

volumeBindingMode: when binding happens

This setting decides whether the PV is created the instant the PVC appears or deferred until a pod actually needs it — which matters for topology.

   Immediate               -> provision the PV as soon as the PVC is created
                              (risk: PV lands in a zone the pod can't schedule to)

   WaitForFirstConsumer    -> wait until a pod using the PVC is scheduled,
                              then provision in the pod's zone/node
                              (recommended for zonal/topology-aware storage)

Dynamic provisioning: the developer writes only the PVC

With a class in place, the workflow is just a claim. The provisioner creates the PV; the PVC binds to it; the pod mounts the claim.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-claim
spec:
  storageClassName: fast-ssd            # name the class; "" disables dynamic
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  containers:
    - name: app
      image: nginx
      volumeMounts:
        - mountPath: /data
          name: data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: data-claim
kubectl apply -f pvc.yaml
kubectl get pvc data-claim               # Pending until consumer if WaitForFirstConsumer
kubectl apply -f pod.yaml
kubectl get pvc,pv                        # PV auto-created; STATUS Bound

The default StorageClass

A PVC with no storageClassName gets the cluster's default class (via the DefaultStorageClass admission controller). Exactly one class should carry the default annotation.

# mark a class default (and there must be only one default)
kubectl annotate sc fast-ssd storageclass.kubernetes.io/is-default-class="true"

# remove default from another class
kubectl annotate sc old-class storageclass.kubernetes.io/is-default-class="false" --overwrite

storageClassName: "" (empty string) explicitly opts a PVC out of dynamic provisioning, forcing it to bind a pre-made PV.

Expanding a volume

If the class sets allowVolumeExpansion: true, edit the PVC's request to grow it; the CSI driver resizes the backing disk online (filesystem grow may need a pod restart on some drivers).

kubectl patch pvc data-claim -p '{"spec":{"resources":{"requests":{"storage":"40Gi"}}}}'
kubectl get pvc data-claim                # CAPACITY grows once resize completes

End-to-end: a PVC triggers dynamic provisioning

The full flow from claim to a mounted, bound volume in the right zone.

End-to-end example: dynamic provisioning then a live PVC expand

A complete walkthrough: create a WaitForFirstConsumer StorageClass, claim a volume that stays Pending until a Pod needs it, mount it, write data, then grow the PVC online.

Step 1 — create the StorageClass (defer binding, allow expansion).

# sc.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: topology-ssd
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  encrypted: "true"
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
kubectl apply -f sc.yaml
# storageclass.storage.k8s.io/topology-ssd created

kubectl get sc topology-ssd
# NAME           PROVISIONER       RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION
# topology-ssd   ebs.csi.aws.com   Delete          WaitForFirstConsumer   true

Step 2 — create the PVC; it stays Pending (no consumer yet).

# pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-claim
spec:
  storageClassName: topology-ssd
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 10Gi
kubectl apply -f pvc.yaml
kubectl get pvc data-claim
# NAME         STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
# data-claim   Pending                                     topology-ssd   5s

kubectl describe pvc data-claim | grep -A2 Events
# Normal  WaitForFirstConsumer  waiting for first consumer to be created before binding

Step 3 — schedule a Pod that mounts the claim; provisioning fires.

# pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: writer
spec:
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "echo hello > /data/marker && sleep 3600"]
      volumeMounts:
        - mountPath: /data
          name: data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: data-claim
kubectl apply -f pod.yaml
kubectl wait --for=condition=Ready pod/writer --timeout=120s

kubectl get pvc,pv
# NAME                               STATUS   VOLUME       CAPACITY   ACCESS MODES   STORAGECLASS
# persistentvolumeclaim/data-claim   Bound    pvc-8a3f..   10Gi       RWO            topology-ssd
# NAME                          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM
# persistentvolume/pvc-8a3f..   10Gi       RWO            Delete           Bound    default/data-claim

Step 4 — verify the mount and that data persists.

kubectl exec writer -- cat /data/marker
# hello
kubectl exec writer -- df -h /data
# Filesystem      Size  Used Avail Use% Mounted on
# /dev/nvme1n1    9.8G   24K  9.8G   1% /data

Step 5 — expand the PVC online (allowVolumeExpansion: true).

kubectl patch pvc data-claim --type merge \
  -p '{"spec":{"resources":{"requests":{"storage":"30Gi"}}}}'
# persistentvolumeclaim/data-claim patched

# the CSI driver resizes the backing disk, then the filesystem grows
kubectl get pvc data-claim -w
# NAME         STATUS   VOLUME       CAPACITY   ACCESS MODES   STORAGECLASS
# data-claim   Bound    pvc-8a3f..   10Gi       RWO            topology-ssd
# data-claim   Bound    pvc-8a3f..   30Gi       RWO            topology-ssd

kubectl exec writer -- df -h /data
# Filesystem      Size  Used Avail Use% Mounted on
# /dev/nvme1n1     29G   24K   29G   1% /data

Key takeaways

  • A StorageClass = provisioner + parameters; it auto-creates PVs for PVCs.
  • Static = admin hand-makes PVs; dynamic = the class provisions on demand.
  • reclaimPolicy Delete (default) destroys data on PVC delete; use Retain to keep it.
  • volumeBindingMode: WaitForFirstConsumer provisions in the pod's zone — use it for zonal disks.
  • One default class serves PVCs with no class; "" opts a PVC out of dynamic provisioning.

Checklist

  • [ ] Listed StorageClasses and identified the default
  • [ ] Created a PVC with no PV and watched a PV auto-provision
  • [ ] Explained Delete vs Retain reclaim policy
  • [ ] Compared Immediate vs WaitForFirstConsumer binding
  • [ ] Expanded a PVC on a class with allowVolumeExpansion