Hands-On Labs · The Capstone

Build a cluster, start to finish

The CKA doesn't hand you a multiple-choice question about a stuck PersistentVolumeClaim — it hands you a broken cluster and two hours to prove you can run one. Reading a Deployment spec doesn't build that reflex; running one, breaking it, and fixing it does. This hub is where you build it. You'll take taskboard — a small three-piece app (a stateless API, a static web frontend, and a Postgres database) — from an empty kind cluster through five connected parts to a cluster a real cluster administrator would trust: namespaced and access-controlled, running real workloads with real health checks, reachable through a real Ingress, backed by storage that survives a restart, and locked down with least-privilege RBAC. Each part opens by stating exactly what "done" looks like. If you only have an afternoon, the fast path below still gets you real reps across all five.

☺ Explain it like I'm 10

Imagine you're setting up a shared apartment building from scratch — not decorating one unit, the whole building. First you sort out the building itself: how many floors, who holds a key to the front door, which door each tenant's key opens. Then real tenants move in — the API family upstairs, the web family that greets visitors at the door, and the database family in the basement who keep every receipt anyone's handed them. You run mail slots and doorbells between units so the web family can actually ring the API's doorbell instead of shouting through the wall. You bolt the database family's filing cabinets to the floor so an argument two rooms over doesn't scatter last week's receipts. And you hand out keys that only open the doors each tenant needs — nobody gets a master key just because it's easier. Same one building, five jobs, in order.

🦫🦊Your host for this track: Benny the Beaver — the builder. He hands each part to the right specialist (Owl, Pip, Ellie, Timmy) and won't call a part "done" until its own done-when line is actually true. Foxy's troubleshooting instinct — Domain 5, 30% of the exam — is on call for all five parts, not just one.
⚠ Before you start

Every part below is local and throwaway — nothing here touches a cloud account or costs real money. You'll want Docker (or Podman), kind, and kubectl; Part 3 adds the ingress-nginx controller and Part 5 goes easier with helm for a couple of add-ons — nothing here needs a paid tier or a cloud VM. Command flags and chart versions drift — always check each project's current docs against what's printed here. Tear everything down when you're done (kind delete cluster --name taskboard-dev) and nothing lingers on your machine.

The world you're building

☺ Like you're 10: Before any building starts, here's the map: one cluster, one small app with three pieces, two drawers to keep things tidy in, and one repo that grows a new room every part.

The hub you're reading and all five parts build inside one continuous cluster, so nothing you set up in Part 1 goes stale by Part 5. Here is the whole world, once, so you never have to re-derive it:

taskboard-dev (kind cluster)
├── namespace: platform          # ingress-nginx, metrics-server, add-ons from "going further"…
├── namespace: taskboard          # api + web Deployments, postgres StatefulSet, Services, NetworkPolicies, RBAC…
└── (git) taskboard/
    ├── api/, web/, Dockerfile.api, Dockerfile.web   # the bare repo you start with
    ├── manifests/01-cluster/          # Part 1 — cluster foundation
    ├── manifests/02-workloads/        # Part 2 — workloads & config
    ├── manifests/03-networking/       # Part 3 — networking & ingress
    ├── manifests/04-storage/          # Part 4 — storage & stateful apps
    └── manifests/05-security/         # Part 5 — security & RBAC

The cluster itself is created once, from a config that reserves host ports 80 and 443 so Part 3's Ingress has somewhere real to answer from:

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: taskboard-dev
nodes:
  - role: control-plane
    kubeadmConfigPatches:
      - |
        kind: InitConfiguration
        nodeRegistration:
          kubeletExtraArgs:
            node-labels: "ingress-ready=true"
    extraPortMappings:
      - containerPort: 80
        hostPort: 80
      - containerPort: 443
        hostPort: 443
◆ Key idea

Every part opens by stating exactly what state your cluster and repo are in when you arrive (inherited from the part before) and what state they're in when you leave (handed to the next one). If a part ever feels like it's starting from nothing, re-read its opening paragraph — you're missing something the previous part was supposed to leave behind.

The five parts

☺ Like you're 10: Five chapters of the same story. Each one hands the next chapter a cluster that's a little more finished than the one before.

Work them in order. Each part pairs with a slice of the official CKA curriculum, so you always know which domain your hands are earning you reps in. Troubleshooting (Domain 5, 30% — the single largest domain on the exam) never gets a part of its own, because on the real exam it never announces itself as one task either; instead, every part below ends by breaking something on purpose that you have to diagnose with nothing but kubectl describe, kubectl logs, and kubectl get events — same as Foxy would.

#PartPairs withDone when
1Cluster Foundation — 🦉 Owl & 🦫 BennyCluster Architecture, Installation & Configuration (D1, 25%) · kind · kubeadmkubectl get nodes shows a Ready node, both namespaces and a baseline RBAC ServiceAccount exist, and a deliberately broken kubeconfig context gets diagnosed and fixed.
2Workloads & Config — 🦫 Benny & 🐿️ NuttyWorkloads & Scheduling (D2, 15%)a rolling update completes with zero dropped requests, and a Deployment with a broken readiness probe never reaches Ready — caught before it takes traffic.
3Networking & Ingress — 🐦 PipServices & Networking (D3, 20%) · Networking & the CNIweb reaches api by its Service DNS name, an external curl through Ingress reaches both paths, and a default-deny NetworkPolicy blocks exactly the traffic it should.
4Storage & Stateful Apps — 🐘 EllieStorage (D4, 10%) · Storage & the CSIa row written before postgres's Pod is deleted is still there after it reschedules, and a PVC requested against a StorageClass that doesn't exist gets diagnosed straight from kubectl describe pvc.
5Security & RBAC — 🐢 TimmyRBAC & Admission Control · Security: Defense in Depth (RBAC slice of D1)kubectl auth can-i confirms api's ServiceAccount can do exactly what it needs and nothing else, and a manifest requesting root or :latest is refused before it ever schedules.

Deep-dive the concept behind any part via its linked lesson before or after you build — the lesson explains why, the part makes you prove it works. For the mechanics this app is standing on, Platform Engineering's Kubernetes-as-Substrate covers the same object model one altitude up, and for a genuinely adversarial pass at Part 5's RBAC once you're comfortable, DevSecOps' Kubernetes Security Deep Dive walks the same objects from an attacker's-eye view.

kind cluster · taskboard-dev You curl / browser namespace: platform ingress-nginx Ingress controller + metrics-server, add-ons namespace: taskboard web Deployment · nginx NetworkPolicy: deny-by-default api Deployment ×2 postgres StatefulSet ×1 PVC · 1Gi / /api Service DNS: api Service DNS: postgres:5432 Every arrow between workloads crosses a Service name, never a raw Pod IP — Pod IPs still change on reschedule.

The fast path — for an afternoon

☺ Like you're 10: No time for five chapters? Here's the one-page summary of the whole story, still worth doing.

The five parts are the deep version of this track — real manifests, real breakage, real triage. If you only have an afternoon, or want one condensed pass before the exam, work this shorter nine-item version instead. It touches every domain at a lighter depth, using the exact same taskboard/taskboard-dev world as the parts above, so nothing you build here is wasted if you come back and go deep later.

0 / 9 fast-path items complete
0Stand up the ground — 🦉 Professor Owl
Install Docker (or Podman), kind, and kubectl. Create the cluster from the config above with kind create cluster --name taskboard-dev --config kind-config.yaml, which reserves host ports 80/443 for Part 3's Ingress and labels the node ingress-ready=true.
Done when: kubectl cluster-info shows the control plane reachable, and kubectl get nodes shows one Ready node carrying the ingress-ready label.
1Namespaces, and a badge not a master key — 🦉 Owl & 🐢 Timmy
Create the taskboard and platform namespaces. Create a ServiceAccount taskboard-api in taskboard with a Role scoped to reading exactly one named ConfigMap — nothing wider. Verify with kubectl auth can-i, impersonating that ServiceAccount via --as, before you ever run a Pod under it.
Done when: kubectl auth can-i get configmaps -n taskboard --as=system:serviceaccount:taskboard:taskboard-api answers yes, and the same command with delete instead of get answers no.
2A Deployment that behaves — 🦫 Benny
Build taskboard-api:dev and taskboard-web:dev locally, load them straight into the cluster with kind load docker-image taskboard-api:dev --name taskboard-dev, then deploy both with readiness and liveness probes on /healthz and real resources.requests/limits. Roll out a typo'd image tag on purpose and watch the rollout stall instead of replacing healthy Pods.
Done when: kubectl rollout status deployment/api completes clean on a good image, and the bad-tag rollout visibly stalls in rollout status without ever reaching Ready.
3Config out of the image — 🦫 Benny & 🐿️ Nutty
Move api's DB host and port into a ConfigMap and its DB password into a Secret, both mounted via envFrom — no value baked into the image. Label every object consistently (app: taskboard, component: api|web|postgres) and select on those labels everywhere instead of by name.
Done when: kubectl get pods -n taskboard -l app=taskboard,component=api returns exactly the api Pods, and editing the ConfigMap plus a rollout restart changes behavior with zero image rebuild.
4A message that actually arrives — 🐦 Pip
Create ClusterIP Services for web and api. Install ingress-nginx into platform and add an Ingress routing / to web and /api to api. Confirm web reaches api through its Service DNS name, never a hardcoded Pod IP.
Done when: curl localhost/ and curl localhost/api/healthz both succeed through the Ingress, and web's own logs show a call to http://api.taskboard.svc.cluster.local, not an IP address.
5Guardrails on the wire — 🐢 Timmy & 🐦 Pip
Apply a default-deny NetworkPolicy for Ingress traffic across taskboard, then add narrow allow rules: web can reach api on 8080, api can reach postgres on 5432 — nothing else gets in. Run a plain busybox Pod outside those rules and try to curl api directly from it.
Done when: the busybox Pod's curl to api times out, while web's own traffic to api keeps working exactly as before.
6Storage that survives a restart — 🐘 Ellie
Deploy postgres as a single-replica StatefulSet with a PVC-backed volume and a headless Service. Write a row through the api, delete the postgres Pod, and confirm the row survives once it reschedules. Then request a second PVC against a StorageClass name that doesn't exist and diagnose the stuck claim.
Done when: the row is still there after the Pod deletion, and kubectl describe pvc on the broken claim names the missing StorageClass as the reason it's stuck Pending.
7Reading the wreckage — 🦊 Foxy & 🦥 Sol
Break something on purpose — an image typo, a memory limit too low for postgres to boot, or a Service selector that no longer matches any Pod's labels — and diagnose it using only kubectl describe, kubectl logs --previous, and kubectl get events, no guessing. Then right-size api's requests/limits from a real kubectl top pod reading instead of a round guess.
Done when: you can state the root cause of your injected fault in one sentence before you fix it, and api's requests/limits come from an actual kubectl top pod number.
Concept: Troubleshooting (D5, 30%) · A Troubleshooting Methodology · Scheduling & Resource Management · This is the thread every item above already made you pull.
CapstoneProve it worked — the whole Pod Squad
Run kubectl auth can-i --list --as=system:serviceaccount:taskboard:taskboard-api and confirm it's a short list, not cluster-admin. Count your own objects: two namespaces, two Deployments, a StatefulSet, three Services, an Ingress, three NetworkPolicies, a Role and a RoleBinding. Then delete the whole kind cluster and rebuild it from your manifests/ alone, with no memory required, and time how long a from-scratch rebuild actually takes.
Done when: the rebuilt cluster reaches the exact same working state with zero manual kubectl edit, using only the manifests already in your repo — and you have a real number for how long that took.
Concept: What Is Kubernetes, and Why · Certifications · This is where every item above gets proven, not just described.
🦊 Foxy's-eye view

"Notice something: I don't get my own item on that list. That's not an oversight — Troubleshooting is 30% of this exam, the biggest single domain there is, and it never shows up as one clean task on the real thing either. It shows up as 'why is this broken,' hiding inside whatever you were already building. If you did all nine and never once had to run kubectl describe on something that wasn't working, go back and actually break something. The exam will."

What you'll have built

☺ Like you're 10: By the end, your laptop holds a small working version of the whole building this course keeps describing — the floors, the tenants, the mail slots, the bolted-down cabinets, and the keys that only open what they should.

Finish the five parts (or the fast path) and you'll have hands-on reps across every domain of the CKA curriculum, weighted the way the real exam weights it: Cluster Architecture, Installation & Configuration (D1, 25%), Workloads & Scheduling (D2, 15%), Services & Networking (D3, 20%), Storage (D4, 10%), and Troubleshooting (D5, 30%) — the full 100%, since the last one is threaded through every part rather than boxed into one. That matters more here than it would for a multiple-choice exam: the CKA is graded on the end state of a live cluster, not on a bubble sheet, so the only real practice is a cluster you built, broke, and fixed with your own hands. Pair this with this course's Certifications hub and CKA Study Plan once your reps are in, and if the Kubestronaut ladder is the eventual goal, the other nine CNCF certifications plus the LFCS live in this platform's Golden Astronaut course once CKA, CKAD, and CKS are behind you.

⚠ Verify this before you book

The CKA is performance-based — a live terminal against real clusters, not multiple choice — graded on the end state of the cluster, not your working. Duration, price, pass mark, and the exact curriculum weights quoted above all change over time, and this is an independent, unofficial study resource, not affiliated with the CNCF or the Linux Foundation. Confirm the current numbers on the Linux Foundation's own CKA page and the curriculum published at github.com/cncf/curriculum before you register.

🦫 Benny's challenge · going further

Already comfortable? Level up any part: put manifests/ under real GitOps so the cluster reconciles from your repo instead of you running kubectl apply by hand; add a HorizontalPodAutoscaler to api and load-test it into actually scaling; swap the single-node kind cluster for a three-node one and force a real reschedule with kubectl drain; put a service mesh sidecar in front of api and compare its mTLS to your Part 5 NetworkPolicy; or wire real TLS through Ingress with cert-manager instead of plain HTTP.

🎬 At the Pod Squad
🦊

Foxy: Five parts for one little task app? Can't I just skim the fast path and call it done?

🦫

Benny the Beaver: You can, Foxy, and you'll still walk away with real reps. But the fast path is the map — the five parts are the actual territory, with a PVC that really gets stuck and a rollout that really stalls.

🐢

Timmy the Turtle: And I'm not signing off Part 5 until kubectl auth can-i --list for api's ServiceAccount comes back short. A long list there is a long list of ways to lose the whole cluster to one bug in one service.

👺

Gizmo the Gremlin: Or... skip the Role entirely, bind cluster-admin straight to the ServiceAccount, ship it Friday, nobody's ever denied anything. 🤑

🐢

Timmy the Turtle: That's not "nobody's denied" — that's "one bug in api is now a bug with root on the entire cluster." Absolutely not.

🐘

Ellie the Elephant: Whichever part you're on, I'm logging it either way — what broke, what fixed it, how long it took. Ask me in a month and I'll still have the record.

🦉

Professor Owl: One cluster, five parts, one loop. Go build it.

🐢 Timmy's checkpoint

1. Which part proves your storage is actually durable, and what two-part test proves it? 2. Which part stops a compromised api Pod from becoming a compromise of the whole cluster? 3. Why doesn't Troubleshooting get a part of its own, even though it's worth more of the exam (30%) than any other single domain? 4. What's the one command that tells you whether a RoleBinding actually grants what you think it grants, without handing out a real credential to find out?

Check your answers
  1. Part 4 — Storage & Stateful Apps. The two-part test: a row written before you delete the postgres Pod is still there after it reschedules, and a PVC requested against a StorageClass that doesn't exist gets caught by kubectl describe pvc instead of hanging silently.
  2. Part 5 — Security & RBAC. Its least-privilege Role means a compromised api ServiceAccount can only do the handful of things api itself actually needs — not everything a cluster-admin binding would allow.
  3. Because on the real exam it doesn't announce itself as one clean task either — it hides inside whatever you were already building. Every part above ends by breaking something on purpose so the reflex gets built five separate times, not memorized once.
  4. kubectl auth can-i, which can impersonate any subject with --as (and --as-group) before you ever grant that subject a real credential.