StorageClasses and dynamic provisioning

How a StorageClass turns a claim into a volume, what each class field controls, how the default class is chosen and changed, and how a claim grows.

Storage · 10%35 min3 tasks

After this lesson

  • 01You can write a StorageClass with the right provisioner, reclaim policy and binding mode, and predict when the PV will appear.
  • 02You can change which class is the cluster default and explain what happens to a claim that names no class.
  • 03You can trace a dynamically provisioned volume from the claim to the PV to the directory on the node.

This lesson is the competency Implement storage classes and dynamic volume provisioning. On the exam it shows up as "create a StorageClass with these properties", "create a claim that uses it and a Pod that mounts it", or "make this class the default". Each is two minutes if you know the five fields of a class and the one annotation that marks a default. The lab's standard class behaves like a real cloud class in every way that matters, including the one that confuses people most: a claim that stays Pending until a Pod needs it.

Anatomy of a StorageClass

A class that keeps volumes and waits for a Pod before provisioningyaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-retain
provisioner: rancher.io/local-path
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: false
parameters: {}        # provisioner-specific: type: gp3, fsType: ext4, ...
FieldDefaultWhat it controls
provisionerrequiredWhich driver creates volumes: a CSI driver name such as ebs.csi.aws.com, an external provisioner such as rancher.io/local-path, or kubernetes.io/no-provisioner for classes that only group static PVs
parametersnoneOptions passed to the provisioner, different per driver (disk type, IOPS, filesystem)
reclaimPolicyDeleteCopied onto every PV the class creates: Delete or Retain
volumeBindingModeImmediateImmediate provisions when the claim is created; WaitForFirstConsumer provisions when a Pod using the claim is scheduled
allowVolumeExpansionfalseWhether claims of this class may grow their request
mountOptionsnoneMount flags applied to every PV of the class
allowedTopologiesnoneRestricts which zones or nodes the provisioner may use

There is no kubectl create storageclass; the YAML comes from the docs or from kubectl get sc standard -o yaml with the metadata noise removed. provisioner, parameters, reclaimPolicy and volumeBindingMode are immutable after creation, so a class with a wrong value is deleted and recreated; existing PVs keep working because they only carry the class name as a string. The default-class annotation and allowVolumeExpansion can be changed in place.

Immediate versus WaitForFirstConsumer

With Immediate the provisioner acts as soon as the claim exists. For storage that is tied to a place (a local disk, a cloud disk in one zone) that is a problem: the volume lands somewhere before the scheduler knows where the Pod can run, and the Pod may then be unschedulable. WaitForFirstConsumer fixes that by provisioning only after a Pod that uses the claim has been scheduled, so the scheduler picks the node first and the volume is created where the Pod is. The cost is cosmetic but confusing: the claim shows Pending with the event waiting for first consumer to be created before binding until a Pod references it. That is correct behavior, not a fault. On this lab standard is WaitForFirstConsumer, so a claim alone never binds; create the Pod and both go Bound together.

The default class

A class becomes the default through the annotation storageclass.kubernetes.io/is-default-class: "true"; kubectl get sc marks it (default). When a claim leaves storageClassName out, the API server fills in the default at admission; if there is no default at that moment, the claim keeps an empty class and receives the default retroactively once one is created (stable since v1.28). A claim with storageClassName: "" opts out of all of this and only binds to classless static PVs. If two classes carry the annotation, the most recently created one is used, so when you switch defaults, clear the old one as well. Changing the default is two patches and nothing else restarts.

Switch the default class and confirmbash
kubectl get sc
kubectl patch storageclass standard -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
kubectl patch storageclass st-bulk  -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
kubectl get sc                                   # exactly one "(default)"
kubectl get sc st-bulk -o jsonpath='{.metadata.annotations.storageclass\.kubernetes\.io/is-default-class}'

What happens when a claim is created

  1. The claim is admitted; a missing storageClassName is replaced by the default class.
  2. If the class is WaitForFirstConsumer, nothing happens until a Pod that uses the claim is scheduled; the scheduler then records the chosen node on the claim.
  3. The provisioner named in the class, which runs as a Deployment or DaemonSet in the cluster and watches claims, creates the storage (a disk, a directory, an NFS export) and a PV object named pvc-<claim uid> with the class's reclaimPolicy and a claimRef back to the claim.
  4. The PV controller binds claim and volume; both show Bound.
  5. The kubelet on the Pod's node attaches and mounts the volume and starts the container.
  6. When the claim is deleted, a Delete PV is removed along with its storage; a Retain PV goes to Released and keeps everything.
Trace a volume on the lab, from claim to node directorybash
kubectl -n st-sc get pvc cache                                   # Pending until a Pod uses it
kubectl -n st-sc describe pvc cache | tail -5                      # the WaitForFirstConsumer event
kubectl -n st-sc get pvc cache -o jsonpath='{.spec.volumeName}'    # pvc-<uid> once bound
kubectl get pv $(kubectl -n st-sc get pvc cache -o jsonpath='{.spec.volumeName}') -o yaml | grep -A3 -E 'hostPath:|nodeAffinity:|reclaimPolicy'
# local-path keeps the data on the node the Pod was scheduled to:
docker exec cka-worker ls /var/local-path-provisioner/             # pvc-<uid>_st-sc_cache
# The provisioner itself is an ordinary Deployment:
kubectl -n local-path-storage get pods

CSI drivers and in-tree provisioners

Storage drivers used to be compiled into Kubernetes (the in-tree plugins with kubernetes.io/ provisioner names such as kubernetes.io/aws-ebs). Those cloud plugins have been removed; their classes keep working only through CSI migration, which silently routes them to the equivalent CSI driver. New classes name the CSI driver directly: ebs.csi.aws.com, pd.csi.storage.gke.io, disk.csi.azure.com. A CSI driver is a set of Pods (a controller Deployment plus a node DaemonSet) registered through a CSIDriver object; kubectl get csidrivers and kubectl get csinodes show what a cluster has. Provisioners that are not CSI still exist: rancher.io/local-path on this lab is an external provisioner that writes hostPath PVs, and kubernetes.io/no-provisioner is the placeholder for classes that only label static local PVs so they can use WaitForFirstConsumer.

Expanding a claim

If the class has allowVolumeExpansion: true and the driver supports it, you grow a volume by raising spec.resources.requests.storage on the claim, never on the PV. The driver resizes the backend, then the filesystem is grown either online or at the next mount; while that is pending the claim carries a FileSystemResizePending condition and status.capacity still shows the old size. Shrinking is not allowed, though recent releases let you lower a request that the backend refused, back down to the current size. On this lab the attempt is refused outright, which is a useful thing to have seen: standard does not allow expansion, so the API server rejects the edit with only dynamically provisioned pvc can be resized and the storageclass that provisions the pvc must support resize.

Grow a claim where the class allows itbash
kubectl get sc fast -o jsonpath='{.allowVolumeExpansion}'          # must be true
kubectl -n app patch pvc data -p '{"spec":{"resources":{"requests":{"storage":"5Gi"}}}}'
kubectl -n app get pvc data -o jsonpath='{.status.capacity.storage}{" "}{.status.conditions[*].type}'
# Interactive: kubectl edit pvc data -n app and change requests.storage

In the docs · the tab you get on exam day

Taskst-sc-1
Storage3%~4 min

context · kubectl config use-context kind-cka

Create a StorageClass

Create a StorageClass named fast-retain that uses the provisioner rancher.io/local-path, keeps volumes after their claim is deleted, and delays volume binding until a Pod that uses the claim has been scheduled. It must not be the cluster's default class.
  1. 01

    Set up the scenario

    curl -fsSL https://codewithmuh.com/labs/cka/t/st-sc-1/setup | bash
  2. 02

    Solve it in your terminal

    Work the way the exam makes you: kubectl, the docs, nothing else. Put any answer files under /tmp/cka/.

  3. 03

    Check your work

    curl -fsSL https://codewithmuh.com/labs/cka/t/st-sc-1/verify | bash
    Counts toward your readiness score.
  4. 04

    Clean up

    curl -fsSL https://codewithmuh.com/labs/cka/t/st-sc-1/cleanup | bash
Taskst-sc-2
Storage5%~7 min

context · kubectl config use-context kind-cka

Provision a volume dynamically from a class

In namespace st-sc create a PersistentVolumeClaim named cache that requests 500Mi with access mode ReadWriteOnce from the StorageClass fast-retain. Then create a Pod named cache-user with image busybox:1.36 and command sh -c 'echo cached > /cache/marker; while true; do sleep 3600; done' that mounts the claim at /cache. When you are done the claim must be Bound, and the PersistentVolume that backs it must have reclaim policy Retain.
  1. 01

    Set up the scenario

    curl -fsSL https://codewithmuh.com/labs/cka/t/st-sc-2/setup | bash
  2. 02

    Solve it in your terminal

    Work the way the exam makes you: kubectl, the docs, nothing else. Put any answer files under /tmp/cka/.

  3. 03

    Check your work

    curl -fsSL https://codewithmuh.com/labs/cka/t/st-sc-2/verify | bash
    Counts toward your readiness score.
  4. 04

    Clean up

    curl -fsSL https://codewithmuh.com/labs/cka/t/st-sc-2/cleanup | bash
Taskst-sc-3
Storage3%~4 min

context · kubectl config use-context kind-cka

Change the default StorageClass

The cluster's default StorageClass is standard. A class named st-bulk has been added. Make st-bulk the default StorageClass so that claims which do not name a class use it, and make sure standard is no longer marked as default. Exactly one class may be marked default when you finish. Do not delete or recreate either class.
  1. 01

    Set up the scenario

    curl -fsSL https://codewithmuh.com/labs/cka/t/st-sc-3/setup | bash
  2. 02

    Solve it in your terminal

    Work the way the exam makes you: kubectl, the docs, nothing else. Put any answer files under /tmp/cka/.

  3. 03

    Check your work

    curl -fsSL https://codewithmuh.com/labs/cka/t/st-sc-3/verify | bash
    Counts toward your readiness score.
  4. 04

    Clean up

    curl -fsSL https://codewithmuh.com/labs/cka/t/st-sc-3/cleanup | bash

Finished reading?

0 of 3 tasks passed. Readiness counts tasks, so run them before you move on.