Catalog
All 84 stages in one place — filter by difficulty or verification status, or search by stage, description or the concept it teaches. Click any row for the stage's note and the diff the reference solution adds at that stage.
- 84Stages
- 84✓ Verified
- 20Hard
- 3Courses
What "verified" means: ✓ Verified — the stage has a reference solution checked in that passed stages 1..N cumulatively against a live kind cluster in CI. CI re-verifies every snapshot on each push, so a drifted assertion fails the build rather than the reader.
Status
| ✓ | Stage | Level | What it teaches |
|---|---|---|---|
| Build your own kubectl 30/30 verified | |||
| ▤ | Primer | Background | kubectl is not special.▸ |
| ✓ | 1. Find the cluster | Easy | Before a client can do anything it has to answer "which server, as whom?", and the answer is never hardcoded.▸ |
| ✓ | 2. Choose a context | Easy | A context is a named triple: a cluster, a user, and optionally a namespace.▸ |
| ✓ | 3. Ask the server what it is | Medium | This is the first request that leaves your machine, and it deliberately goes through the discovery client rather than a typed one.▸ |
| ✓ | 4. List the pods | Easy | The first real read. A typed clientset gives you a method per resource — CoreV1().Pods(ns).List(...) — which returns *corev1.PodList with real Go fields.▸ |
| ✓ | 5. Choose a namespace | Medium | Namespace resolution has three steps and the middle one is the trap: --namespace, then whatever the current context carries, then "default".▸ |
| ✓ | 6. Print a table | Medium | A table is a user interface, and the property that makes it one is that the columns line up when the names do not.▸ |
| ✓ | 7. Humanise the age | Medium | The API stores a creation timestamp; a person reading a hundred rows wants "2d".▸ |
| ✓ | 8. Print the object as JSON | Medium | -o json is not "your table, as JSON". It is the API's own object, verbatim — apiVersion, kind, metadata, spec, status, managedFields and all.▸ |
| ✓ | 9. Print the object as YAML | Medium | Same object, different serializer — and the way you get there matters.▸ |
| ✓ | 10. Look everywhere at once | Medium | -A is not a client-side filter and it is not a loop over namespaces.▸ |
| ✓ | 11. Ask for one object | Easy | Get and List are different endpoints with different failure modes, and the difference is the lesson. A List of an empty namespace succeeds and returns nothing.▸ |
| ✓ | 12. Ask what the server serves | Medium | This is the request that makes a client general. Nothing in your program knows what a Pod is: the server reports its groups, the versions in each, and the resources in those —…▸ |
| ✓ | 13. Resolve any spelling | Hard | po, pod, pods, Pod, pods.v1. — five spellings of one thing, and the API accepts exactly one of them in a URL.▸ |
| ✓ | 14. Get anything at all | Hard | This is the largest single step in the course: your program stops being a pod client. The dynamic client speaks in GVRs and unstructured.▸ |
| ✓ | 15. Wider columns | Easy | -o wide adds the columns that only make sense for a particular resource: a Pod's node and IP, a Service's cluster IP and ports.▸ |
| ✓ | 16. Filter by label | Medium | Labels are the identifying metadata everything in Kubernetes coordinates through: a Service finds its pods by selector, a Deployment owns its ReplicaSets by selector.▸ |
| ✓ | 17. Filter by field | Medium | Field selectors look like label selectors and are a different axis.▸ |
| ✓ | 18. A stable order | Easy | The API returns items in whatever order the storage layer hands them over.▸ |
| ✓ | 19. Keep watching | Hard | A watch is a long-lived HTTP response the server keeps appending to, one JSON event per change: ADDED, MODIFIED, DELETED, BOOKMARK, ERROR.▸ |
| ✓ | 20. Delete an object | Easy | The first write, and the lesson is that delete is asynchronous. The call returns when the server has accepted the request and marked the object — not when it is gone.▸ |
| ✓ | 21. Create from a manifest | Hard | A manifest is self-describing: it carries apiVersion and kind, which is why kubectl create -f needs no flag saying what is in the file.▸ |
| ✓ | 22. Apply, not create | Hard | Apply is not "create if missing, update if present". It is a different claim: these fields should look like this, and I am the one saying so.▸ |
| ✓ | 23. Two managers, one field | Hard | When your apply sets a field another manager already owns, the server answers 409 Conflict and changes nothing.▸ |
| ✓ | 24. Change one field | Medium | Patch is the smallest write verb: the body names only what changes, so nothing has to be read first and there is no resourceVersion to conflict on.▸ |
| ✓ | 25. Scale through a subresource | Medium | /scale is a subresource: a separate endpoint on the same object, with its own tiny schema.▸ |
| ✓ | 26. Describe one object | Medium | Describe answers a different question from get, and the difference explains the format.▸ |
| ✓ | 27. Attach its events | Hard | Events are the cluster's explanation of why, and the surprise is that they are not part of the object they describe.▸ |
| ✓ | 28. Stream a container's logs | Hard | There is no Log object to Get. pods/log is a subresource that returns a stream of bytes, and with --follow it never ends — which is why its client-go signature could not have…▸ |
| ✓ | 29. Run a command inside | Hard | Exec needs several independent byte streams at once — stdin, stdout, stderr and a terminal-resize channel — multiplexed over a single connection. Ordinary HTTP/1.▸ |
| ✓ | 30. Forward a local port | Hard | Port-forward is the same upgrade as exec put to a different use: a local listener whose connections are tunnelled, one stream pair per forwarded port, through the apiserver to the…▸ |
| Build your own controller 28/28 verified | |||
| ▤ | Primer | Background | Kubernetes is not a system that does what you tell it.▸ |
| ✓ | 1. Teach the cluster a new kind | Medium | A Kubernetes API is not a fixed list. A CustomResourceDefinition is an ordinary object that, once stored, makes the API server serve a whole new resource: /apis/byok8s.▸ |
| ✓ | 2. Give the kind a schema | Medium | The schema is not documentation. It is the contract the API server enforces on your behalf: a Website with replicas: "two" is refused before it is ever stored, and a Website with…▸ |
| ✓ | 3. List, then watch from there | Medium | A watch is not a feed of everything that ever happened. It is a stream of changes after a point in time, and that point is a resourceVersion.▸ |
| ✓ | 4. Let an informer keep the cache | Medium | The list-then-watch you just wrote is what an informer does for you — and then it does the part you did not write: it keeps everything it has seen in a local store, so the…▸ |
| ✓ | 5. Read from the cache | Easy | Once the informer holds the objects, asking the API server for them again is pure waste.▸ |
| ✓ | 6. Queue keys, not objects | Medium | Handlers must not do work. They run on the informer's thread, and anything slow in one blocks every other event.▸ |
| ✓ | 7. Reconcile, don't react | Medium | A reconcile function is handed a key and answers one question: given how the world is right now, what should I do? It does not ask what happened.▸ |
| ✓ | 8. Create the Deployment | Medium | Here the controller stops observing and starts acting. A Website is a wish; a Deployment is the thing that grants it.▸ |
| ✓ | 9. Own what you create | Medium | The controller never writes deletion code for its children. It writes down who owns what, and the API server's garbage collector does the rest: when the Website goes, everything…▸ |
| ✓ | 10. Adopt what is already there | Medium | Sooner or later the controller meets a Deployment with the right name that belongs to nobody: someone created it by hand, or an earlier version of this controller did not set…▸ |
| ✓ | 11. Put back what drifts | Medium | Someone runs kubectl scale. The Deployment now says 0 and the Website still says 2.▸ |
| ✓ | 12. Roll out a spec change | Easy | Editing the Website changes the wish, and the same repair loop that fixed a hand-scaled Deployment now rolls out a new image.▸ |
| ✓ | 13. Add the Service | Easy | One wish, several objects. A Website is not "a Deployment with extra steps" — it is a thing a person wants, and granting it takes a Deployment and a Service, and later possibly…▸ |
| ✓ | 14. Retry instead of dying | Medium | A pass fails. Not because your code is wrong — because the spec asks for something the cluster refuses, or a webhook is down, or a quota is full.▸ |
| ✓ | 15. Reconcile on a timer | Medium | Nothing has to happen for a Website to stop being satisfied. A hand-run kubectl scale changes the child, and no event about the parent will ever arrive — so a controller that…▸ |
| ✓ | 16. Let someone pause you | Easy | At three in the morning, with a bad spec rolling out, the only lever a person has is your controller.▸ |
| ✓ | 17. Report what you did | Medium | Spec is the wish. Status is the world. Keeping them apart is not tidiness — the status subresource makes the API server enforce it: a write to the main endpoint cannot change…▸ |
| ✓ | 18. Say what Ready means | Medium | A condition is a yes/no question with a reason attached, and the reason is the useful half. "Not ready" is a status light.▸ |
| ✓ | 19. Clean up before you let go | Hard | Garbage collection handles anything inside the cluster that carries an owner reference.▸ |
| ✓ | 20. Lose a race and recover | Hard | Kubernetes has no locks. Every write carries the resourceVersion the writer read, and the API server rejects it if the object has moved on — HTTP 409, IsConflict.▸ |
| ✓ | 21. Apply as a field manager | Hard | Everything so far has been "check, then create or patch". Server-side apply replaces that with one call: you send the object as you want it, the API server records which fields…▸ |
| ✓ | 22. Watch what you own | Medium | The timer bounds how long the world can be wrong. Watching your children makes it right almost immediately: someone deletes the Deployment, an event arrives, and the pass that…▸ |
| ✓ | 23. Leave other people's objects alone | Medium | A controller that matches objects by name is a controller that will eventually overwrite something it did not create.▸ |
| ✓ | 24. Record events | Medium | Status says what is true now. Events say what happened, and they are what kubectl describe shows at the bottom — the first place anyone looks when something is not working.▸ |
| ✓ | 25. Expose the loop's metrics | Easy | A controller that is quietly failing looks exactly like a controller with nothing to do.▸ |
| ✓ | 26. Answer healthz and readyz | Easy | Once this runs in the cluster, the kubelet decides whether to restart it and whether to send it traffic, and it decides by asking. Two endpoints, two different questions:▸ |
| ✓ | 27. Elect a leader | Hard | Run two copies of this controller and they will both reconcile every Website: two writers, conflicting updates, children created twice under different names if you are unlucky.▸ |
| ✓ | 28. Shut down cleanly | Medium | A controller is stopped constantly — every rollout, every node drain, every scale-down.▸ |
| Build your own admission webhook 26/26 verified | |||
| ▤ | Primer | Background | Every write to a Kubernetes cluster takes the same road.▸ |
| ✓ | 1. Serve HTTPS the API server will trust | Medium | An admission webhook is called, not calling. Before it can judge anything it has to be a server the API server is willing to talk to — and the API server has exactly one rule…▸ |
| ✓ | 2. Read an AdmissionReview, answer with one | Medium | The payload of admission is one kind, AdmissionReview, and it travels in both directions. The API server POSTs one with .request filled in; you reply with one with .▸ |
| ✓ | 3. Register yourself with the cluster | Medium | A webhook that nobody has been told about is a web server. What makes it part of admission is a ValidatingWebhookConfiguration: a cluster-scoped object that says which requests to…▸ |
| ✓ | 4. Reject what breaks the rule | Easy | Everything so far said yes. A validating webhook earns its name by saying no, and the mechanics of no are smaller than they look: allowed: false, plus a status explaining why.▸ |
| ✓ | 5. Echo the UID, or the answer is discarded | Easy | Your program has two ways to answer, and only one of them is admission.▸ |
| ✓ | 6. Ask only for the objects you judge | Medium | A rule is the blast radius of your webhook, written down. Four axes decide what the API server diverts to you:▸ |
| ✓ | 7. Leave other namespaces alone | Medium | A rule says what kind of object you judge. A namespaceSelector says where — and it is the field that decides whether a mistake in your webhook is an inconvenience or an outage.▸ |
| ✓ | 8. Narrow by label on the object itself | Easy | namespaceSelector filters by where an object lives. objectSelector filters by what it is — the labels on the object being admitted.▸ |
| ✓ | 9. Change an object with a JSON patch | Hard | A mutating webhook answers the same question with one extra field: not just "may this be written", but "write this instead".▸ |
| ✓ | 10. Fill in what was left out | Medium | Defaulting is mutation with a policy attached: instead of refusing an object for what it lacks, you supply it.▸ |
| ✓ | 11. Add a container nobody asked for | Hard | Injecting a container is the most useful and most dangerous thing a mutating webhook does.▸ |
| ✓ | 12. Honour a dry run, change nothing | Medium | kubectl apply --dry-run=server asks the cluster what would happen. The request runs the whole admission chain, including your webhook, and is thrown away instead of being stored.▸ |
| ✓ | 13. Decide what happens when you are down | Medium | failurePolicy answers one question: when the API server cannot get an answer out of you, does the request go through?▸ |
| ✓ | 14. Answer inside the deadline, or be one | Medium | Every request your rules match waits for you. That is the whole cost model of admission, and timeoutSeconds is the bound on it:▸ |
| ✓ | 15. Run again after another webhook has been | Hard | Mutating webhooks run in sequence, in an order you do not control.▸ |
| ✓ | 16. Replace the certificate without dropping a request | Hard | Certificates expire. A webhook whose certificate expires stops being callable, and — depending on the failure policy — either silently stops enforcing or stops the cluster…▸ |
| ✓ | 17. Say why, where an auditor will read it | Easy | A verdict answers one person: whoever ran the command. Two other audiences need to know what you decided, and both have a field.▸ |
| ✓ | 18. Filter with CEL before the request reaches you | Medium | Rules and selectors filter by shape: which resource, which namespace, which labels.▸ |
| ✓ | 19. The same rule, with no webhook at all | Hard | Everything you have built exists because the API server had no way to evaluate a rule itself. Since 1.▸ |
| ✓ | 20. Never gate the namespace you run in | Medium | Stage 7 narrowed this webhook to one namespace. This stage is about the one namespace it must never contain: its own.▸ |
| ✓ | 21. An eviction is not a delete | Hard | Your rule says resources: ["pods"], and for twenty stages that has been enough. It does not cover an eviction.▸ |
| ✓ | 22. Judge what is being taken away | Medium | Stage 21 stopped a drain from taking a protected pod. It left the front door open: kubectl delete pod still works, because the rule covers CREATE and UPDATE and an eviction, and a…▸ |
| ✓ | 23. Guard every way in | Medium | Stages 21 and 22 closed the two ways a protected pod could be taken away. Neither stops anyone getting inside it.▸ |
| ✓ | 24. Let the namespace go | Medium | Stage 22 taught the webhook to refuse the deletion of a protected pod. It does exactly that — including when nobody meant to delete that pod on its own.▸ |
| ✓ | 25. The rule, as data | Medium | Stage 19 moved the owner rule into the API server. It is still a rule written down in code, though: the program builds the policy, so changing what the policy accepts means…▸ |
| ✓ | 26. Every version of the same thing | Medium | A rule names API versions. So far that has been harmless: pods have exactly one.▸ |
No stages match — clear the search or change filters.
The rest of the ladder
Planned courses, in the order they build on each other. They have no stages here yet — the full stage outline for each lives in CURRICULUM.md.
the client
- Build your own diff and safe apply Server-side dry-run, a three-way diff, and a change you can read before it lands.
control loops
- Build your own scheduler Filter, score, bind — then priority and preemption.
the API plane
- Build your own kube-apiserver REST over etcd, watch, server-side apply, RBAC — driven by real kubectl.
the node plane
- Build your own container runtime Image layers, namespaces, cgroups, and the CRI a kubelet can drive.
- Build your own kubelet Register a node, run the pods assigned to it, report what happened.
- Build your own CNI plugin veth pairs, IPAM, routes, and an overlay across nodes.
- Build your own kube-proxy Services and EndpointSlices turned into a working data plane.
the ecosystem
- Build your own Helm Charts, values, releases, and a three-way-merge upgrade.
- Build your own kustomize Bases, overlays, patches, and generated names that stay wired up.
- Build your own CSI driver Provision a volume, stage it, publish it into a pod.
- Build your own metrics server and HPA Scrape the kubelet, serve metrics.k8s.io, then do the scaling maths.
- Build your own cluster bootstrap PKI, static pods, bootstrap tokens — a control plane from nothing.