DEV Community

The Cyber Sidekick
The Cyber Sidekick

Posted on

Add a WaitForFirstConsumer StorageClass and hand it the default (CKA)

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)