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.
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
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, ...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.
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
- The claim is admitted; a missing
storageClassNameis replaced by the default class. - 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. - 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'sreclaimPolicyand aclaimRefback to the claim. - The PV controller binds claim and volume; both show
Bound. - The kubelet on the Pod's node attaches and mounts the volume and starts the container.
- When the claim is deleted, a
DeletePV is removed along with its storage; aRetainPV goes toReleasedand keeps everything.
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 podsCSI 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.
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.storageIn the docs · the tab you get on exam day
- Storage Classes ↗search: “storage classes”
- Dynamic Volume Provisioning ↗search: “dynamic volume provisioning”
- Change the default StorageClass ↗search: “change default storage class”
context · kubectl config use-context kind-cka
Create a StorageClass
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.- 01
Set up the scenario
curl -fsSL https://codewithmuh.com/labs/cka/t/st-sc-1/setup | bash - 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/. - 03
Check your work
curl -fsSL https://codewithmuh.com/labs/cka/t/st-sc-1/verify | bashCounts toward your readiness score. - 04
Clean up
curl -fsSL https://codewithmuh.com/labs/cka/t/st-sc-1/cleanup | bash
context · kubectl config use-context kind-cka
Provision a volume dynamically from a class
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.- 01
Set up the scenario
curl -fsSL https://codewithmuh.com/labs/cka/t/st-sc-2/setup | bash - 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/. - 03
Check your work
curl -fsSL https://codewithmuh.com/labs/cka/t/st-sc-2/verify | bashCounts toward your readiness score. - 04
Clean up
curl -fsSL https://codewithmuh.com/labs/cka/t/st-sc-2/cleanup | bash
context · kubectl config use-context kind-cka
Change the default StorageClass
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.- 01
Set up the scenario
curl -fsSL https://codewithmuh.com/labs/cka/t/st-sc-3/setup | bash - 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/. - 03
Check your work
curl -fsSL https://codewithmuh.com/labs/cka/t/st-sc-3/verify | bashCounts toward your readiness score. - 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.