Add a WaitForFirstConsumer StorageClass and hand it the default (CKA)
There is a version of this CKA task that takes ninety seconds, and a version that quietly breaks the cluster you are being graded on. Today: add a StorageClass, set its binding mode so volumes wait for the pod that needs them, and make it the cluster default. The interesting part is that last word. Default is an annotation, only one class is allowed to carry it, and you are told not to touch anything that already exists. Let's run it on a live cluster.
🎥 Watch the video: https://www.youtube.com/watch?v=5Pbq42ykbn4
This is a CKA Storage walkthrough. Every command below is real output from a live cluster, and you can reproduce the whole thing yourself (scripts at the end).
The scenario
Here is the setup. The cluster already runs a dynamic provisioner, and there are already StorageClasses using it, one of which is marked as the default. Three things are asked of you. Add a class called fast-local that reuses that same provisioner and binds with WaitForFirstConsumer. Make fast-local the cluster's default, which means exactly one class ends up carrying the marker. And do all of that without editing any existing Deployment or PersistentVolumeClaim, because there is a workload here already using storage and points come off if you disturb it.
- 1. New StorageClass 'fast-local': reuse the EXISTING provisioner, WaitForFirstConsumer
- 2. Make it the cluster default (and only one class may be the default)
- 3. Do NOT modify any existing Deployment or PVC
- A running workload in ns 'ledger' is already bound through the old default
How binding modes and the default class work
Two mechanics to have straight before typing. First, binding mode. Immediate means the provisioner creates a volume as soon as the claim appears, before anyone knows which node the pod will land on. WaitForFirstConsumer means the claim sits Pending on purpose until a pod that mounts it gets scheduled, and only then is the volume created, on that pod's node. For node-local storage that is the correct choice, and it is what this task asks for. Second, the default. It is not a field in the spec, it is an annotation, storageclass.kubernetes.io/is-default-class, set to the string true. It only affects claims that name no class at all, and a cluster with two of them is misconfigured. So the order matters: take it off the old class first, then give it to the new one.
What you are handed
Read the cluster before changing it. There are two classes already. Both use the same provisioner, rancher.io/local-path, which is the one the task tells you to reuse, so there is nothing to install. Standard carries the default marker, and it binds with WaitForFirstConsumer; bulk-hdd binds Immediately and is not the default. Note which one is default, because that is the class you will have to take it away from. Then look at what you must not break: the ledger namespace has a claim that is already Bound through standard, with a Deployment running on top of it.
$ kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
bulk-hdd rancher.io/local-path Delete Immediate false 4m24s
standard (default) rancher.io/local-path Delete WaitForFirstConsumer false 4m24s
$ kubectl get pvc,pods -n ledger
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
persistentvolumeclaim/ledger-data Bound pvc-8bf9e803-20a7-4015-aa77-5d3879dcf33d 128Mi RWO standard <unset> 4s
NAME READY STATUS RESTARTS AGE
pod/ledger-api-6bff9497c6-jw27j 1/1 Running 0 4s
Create the class
Now the class itself. In the exam, open the StorageClass page in the Kubernetes docs, copy the example, and delete everything the task did not ask for: no parameters, no mount options, no reclaim policy. What is left is four lines that matter. The name, fast-local. The provisioner, copied exactly from the class that already exists, because reusing it is the whole point. And volumeBindingMode set to WaitForFirstConsumer. Apply it, and the class is created. Notice there is no default annotation on it yet; that is deliberate, and it is the next step.
$ cat fast-local.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-local
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
$ kubectl apply -f fast-local.yaml
storageclass.storage.k8s.io/fast-local created
Unset the old default
Before handing the title over, take it away from the current holder. A one line patch sets the annotation on standard to the string false. kubectl edit gets you to the same place if you prefer an editor, and in the exam either is fine. Look at the list now: nothing carries the default marker. That is a perfectly valid state, and it is much safer than the alternative, because if two classes claim to be the default, Kubernetes cannot choose between them and an unqualified claim will not get a class at all.
$ kubectl patch storageclass standard -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
storageclass.storage.k8s.io/standard patched
$ kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
bulk-hdd rancher.io/local-path Delete Immediate false 4m25s
fast-local rancher.io/local-path Delete WaitForFirstConsumer false 0s
standard rancher.io/local-path Delete WaitForFirstConsumer false 4m25s
Hand over the default
Same patch, other direction: set the annotation to true on fast-local. And now read the list the way a grader would. Exactly one class shows the default marker, it is ours, the provisioner matches the one that was already in the cluster, and the binding mode says WaitForFirstConsumer. Every requirement of the task is visible in that single line of output. If you had baked the annotation into the YAML when you created the class, you would still have had to unset standard, so the order of operations is the thing to remember, not which tool you used.
$ kubectl patch storageclass fast-local -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
storageclass.storage.k8s.io/fast-local patched
$ kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
bulk-hdd rancher.io/local-path Delete Immediate false 4m25s
fast-local (default) rancher.io/local-path Delete WaitForFirstConsumer false 0s
standard rancher.io/local-path Delete WaitForFirstConsumer false 4m25s
Pending on purpose
Let's prove both halves for real, without touching anything we were told to leave alone. Here is a throwaway claim in its own namespace with no storageClassName at all. Create it, and look at the class column: fast-local. Nothing told it that; it picked up the default, which is exactly what the annotation is for. And the status is Pending. That is not a failure, and this is the part people panic about in the exam. Describe the claim and the event says it plainly: waiting for first consumer to be created before binding. The volume does not exist yet because no pod has asked for it yet.
$ kubectl apply -f scratch-claim.yaml
persistentvolumeclaim/scratch-claim created
$ kubectl -n scratch get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
scratch-claim Pending fast-local <unset> 4s
$ kubectl -n scratch describe pvc scratch-claim | grep -A3 'Events:'
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal WaitForFirstConsumer 5s persistentvolume-controller waiting for first consumer to be created before binding
The first consumer binds it
Now create the first consumer, a single pod that mounts that claim. The scheduler places the pod, the provisioner creates the volume on that node, and the claim goes straight to Bound with a real volume behind it. That is the entire point of WaitForFirstConsumer: the storage follows the pod instead of the pod being dragged to wherever the storage happened to land. And the last check is the one the task actually grades you on for part three. The ledger namespace is exactly as we found it: still Bound, still through standard, its Deployment still running. We changed which class is default without editing a single existing object.
$ kubectl apply -f scratch-writer.yaml
pod/scratch-writer created
$ kubectl -n scratch get pvc,pods
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
persistentvolumeclaim/scratch-claim Bound pvc-2bcf522a-4a2e-4eba-9e5d-2887b123165b 64Mi RWO fast-local <unset> 10s
NAME READY STATUS RESTARTS AGE
pod/scratch-writer 1/1 Running 0 5s
$ kubectl get pvc,pods -n ledger
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
persistentvolumeclaim/ledger-data Bound pvc-8bf9e803-20a7-4015-aa77-5d3879dcf33d 128Mi RWO standard <unset> 15s
NAME READY STATUS RESTARTS AGE
pod/ledger-api-6bff9497c6-jw27j 1/1 Running 0 15s
Exam tips
Things worth carrying into the exam. Copy the StorageClass example from the docs and delete every field the task did not mention, because extra fields are extra chances to be wrong. Reuse the provisioner string exactly as it appears in the existing class; do not type it from memory. Remember the annotation value is a quoted string, true, not a boolean. Always unset the old default before setting the new one, and confirm with kubectl get storageclass that exactly one class shows the marker. Do not try to move an existing claim onto your new class: a bound PVC's storage class cannot be changed anyway, and here you would be modifying an object the task explicitly protects. And if a new claim sits Pending on a WaitForFirstConsumer class, that is the design, not a bug.
- Strip the docs example to what was asked: name, provisioner, volumeBindingMode
- Copy the provisioner string from the existing class, do not retype it
- The annotation value is the STRING "true" / "false"
- Unset the old default FIRST, then confirm exactly one (default) marker
- Never re-point an existing PVC: immutable once bound, and off limits here
- Pending on a WaitForFirstConsumer class is expected, not a failure
Recap
- fast-local created: existing provisioner + volumeBindingMode WaitForFirstConsumer
- Default annotation moved off 'standard', onto 'fast-local' (exactly one marker)
- Unqualified claim landed on fast-local, Pending -> Bound on first consumer
- ns 'ledger' untouched, still Bound through 'standard'; subscribe + dev.to writeup
Reproduce this yourself
The entire scenario is scripted on a throwaway kind cluster: https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios
git clone https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios.git
cd TCS_CKA_2026_Exam_Scenarios/learning/scenarios/scenario13-storageclass-default
./setup.sh # creates the cluster AND arms the scenario
# solve it by hand, or:
./solution.sh # apply the answer key and verify
If this helped, subscribe to The Cyber SideKick on YouTube for more CKA drills, and grab the newsletter at https://thecybersidekick.beehiiv.com.
Top comments (0)