cka-sim/README.md

19 KiB

cka-sim — Kubernetes certification exam simulator

Entry point is ./exam (renamed from cka-sim so it can't collide with the directory name).

Hands-on exam tasks graded per check against a real cluster, an exam clock, a browser front end modelled on the real PSI exam, attempt history, and drills that rotate through the domains before you sit a full exam. Tracks: CKA (113 tasks: a random pool of 54 normal + 24 hard, plus two fixed simulator sets, C and D, of 18 and 17 tasks), CKAD (44 pool tasks, plus a 20-task killer.sh-style Set A) and CKS (a 9-task starter bank in the same format). Each track runs on the Kubernetes version its real exam runs — CKA and CKS on v1.35, CKAD on v1.37 (see Clusters and versions).

CKA and CKAD are also laid out as a study path: the curriculum objective by objective, each with notes, at least two pool tasks, and tracked progress from first reading to exam conditions (see Study path).

How a session is built

Every session draws fresh tasks: start samples per domain in the exam's proportions (ARCH 5, WORK 4, NET 4, STOR 2, TSHOOT 5 = 20), preferring tasks you have never seen, then the least-recently seen. Most tasks are parameterized — namespaces, names, replica counts, ports, which node is broken — so even a repeat isn't the same task. drill with no arguments picks the domain you've drilled least and gives you 5 tasks from it; a full exam is gated until every domain has been drilled at least once (--force skips that).

Hard mode (--hard, or the toggle in the UI lobby) is modelled on what people report about killer.sh, not on its content: 25 tasks in the same 120 minutes, hard tasks preferred, no hints about paths or nodes, traps (an over-granting binding you must find, an allow-all NetworkPolicy left behind, a relocated staticPodPath, two PVs where only one is right), exact-output tasks graded byte-for-byte, compound failures (three faults on one node, two control-plane components down for different reasons, DNS broken three ways), collateral-damage checks (still cordoned, recreated instead of fixed in place, extra PVs), and exam-style contexts k8s-c1-H / k8s-c2-AC / k8s-c3-CCC with different default namespaces — each task says which to use (CKA only; they point at the CKA cluster). Grading always runs against the track's own cluster context regardless of which one you're on.

Why kind and not k3s / hosted labs

kind is kubeadm-based: real etcd, static-pod control plane, systemd kubelet on every node, kubeadm certs on the control plane. That is what lets the troubleshooting tasks break real things (kubelet, scheduler manifest) the way the exam does. Hosted labs and CK-X give you no node access, which rules out exactly those tasks. podman exec -it <node> bash plays the role of ssh <node> here; use the Vagrant/ssh mode below if you want a real ssh session.

Requirements

podman, kind (v0.33.0 publishes both node images used here), kubectl, python3, ttyd (for the terminal pane in the browser UI). ~4 GB RAM per 4-node cluster — about 8 GB with both up; stop the one you are not using with podman stop $(podman ps -q --filter label=io.x-k8s.kind.cluster=cka-sim-137) (./setup.sh ckad starts it again). Rootless podman needs cgroup v2; otherwise sudo -E ./setup.sh. Docker still works: CONTAINER_RUNTIME=docker ./setup.sh.

Clusters and versions

The real exams do not share a Kubernetes version, so neither do the tracks. Each Kubernetes minor gets its own kind cluster, and every ./exam command uses the cluster of its track:

Track Real exam runs kind cluster Context Node image
CKA, CKS v1.35 cka-sim-135 kind-cka-sim-135 kindest/node:v1.35.8 (etcd 3.6)
CKAD v1.37 cka-sim-137 kind-cka-sim-137 kindest/node:v1.37.0 (etcd 3.7)

Versions are from the Linux Foundation's exam instructions as of 2026-10-02 (CKAD moved from v1.35 to v1.37 at the end of September; the LF moves each exam on its own schedule). The domains and weights did not change with it — the v1.37 CKAD curriculum is the v1.35 document. When an exam moves, change "k8s" in its TRACK (and add the node image to KIND_IMAGES in ./exam if it is a new minor), then run ./setup.sh <track>: tracks on the same minor share a cluster. The old minor's cluster is then unused — delete it with KIND_EXPERIMENTAL_PROVIDER=podman kind delete cluster --name cka-sim-<old> (plain ./teardown.sh removes every cka-sim cluster).

setup.sh also downloads the kubectl that matches each cluster (.state/kubectl/v1.35/kubectl, ...), because the exam's kubectl matches its cluster and kubectl 1.36+ behaves differently in places (kubectl describe with a selector hides Events, kubectl debug defaults to the general profile). Every session start points the kubectl context and .state/kubectl/current at the track's cluster, and examrc puts current/ first on PATH, so your terminals follow. Only one session can be open at a time across tracks (--force overrides on the command line). ./exam clusters shows what each track uses and whether it is up.

The single cluster cka-sim from before per-version clusters is not used any more: ./teardown.sh legacy deletes it.

Run

./setup.sh [cka|ckad|cks|all]   # create that track's cluster, preload images (idempotent;
                                #   default: the track ./exam defaults to - $CKA_TRACK, else cka)
./exam web                      # open http://127.0.0.1:8080 — start an exam or drill from there

Both the built-in terminal and your own shell can have the exam aliases:

source ./examrc          # bash: k=kubectl, $do, $now, kubectl completion (also for k), vim defaults
source ./examrc.fish     # fish equivalent

./exam web sources the right one automatically for its terminal pane.

That's the whole loop. The UI seeds the scenarios, runs the clock, lets you flag questions and keep notes, submits (or auto-submits at 00:00), shows the score with every check, and offers "Drill what I missed". The terminal pane is a real bash via ttyd with kubectl pointed at the cluster of the session's track (--shell fish if you'd rather practise in fish; the real exam is bash).

The same state is available from the terminal if you prefer:

./exam drill                      # next least-drilled domain, 5 tasks (add --hard)
./exam drill weak | drill 2 17 20 | drill TSHOOT NET | drill setd
./exam start [--hard]             # full exam (gated until all domains drilled; --force)
./exam sim c|d                    # a fixed 18/17-task simulator set, exam rules, 120 min
./exam objectives                 # CKA/CKAD: the curriculum as a path, with your progress
./exam next                       # start whatever is next on that path
./exam study [OBJ|DOMAIN|weak|N..]  # no clock, solutions visible, kept out of the history
./exam notes [OBJ|DOMAIN|all]     # study notes of an objective (3.2 or probes)
./exam list | show N | timer | status                       # during
./exam end                                                  # submit
./exam weak | history                                       # afterwards
./exam --track ckad web                                     # other tracks and their clusters (or CKA_TRACK=ckad)
./exam clusters                                             # Kubernetes version and cluster per track
./teardown.sh [cka|ckad|cks|legacy]                         # delete clusters + state (history kept; default all)

Study path: the curriculum, objective by objective

tracks/cka/curriculum.py and tracks/ckad/curriculum.py hold the exam curricula in their own words and order - 27 objectives for CKA, 24 for CKAD. Every objective has study notes (what to know, the fastest commands, the traps, the documentation pages the exam lets you open) and lists the tasks that practise it; every task of the bank is listed under at least one objective, and every objective has at least two pool tasks.

./exam objectives        # every objective, its tasks, where you are, and the next step
./exam next              # start that next step
./exam notes 3.2         # the notes of one objective, by id or key (3.2, probes); `all`, or a domain
./exam study 3.2         # those notes, then its tasks: no clock, solutions open  (--all adds simulator-set tasks)
./exam drill 3.2         # the same tasks against the clock, no solution until a task is graded

Each objective goes through two stages, and the path ends in the mocks:

  1. Study - solve every task of the objective once, with the notes and the reference solutions open. The objective is then studied (~).
  2. Clock - pass the same tasks as a timed drill (6 minutes per task, new parameters). When the latest attempt at every task is a full pass, the objective is complete (✓).
  3. Mock exam (./exam start) once every objective is complete, then the fixed simulator sets (./exam sim a for CKAD, c and d for CKA) - exam rules, no feedback until you submit.

./exam next always starts the first open step in that order; ./exam objectives shows the whole ladder. An objective with many tasks takes several sittings: each brings up to five tasks you have not solved yet. The CKA path walks the domains Workloads, Networking, Storage, Architecture and then Troubleshooting - it is 30% of the exam and builds on all the others (STUDY_ORDER in its curriculum.py); CKAD follows the curriculum's order. Nothing is locked: study or drill any objective at any time.

Progress is tracked in two places. Timed attempts are in history-<track>.jsonl, as always, and the objective map reads them - so what you have already drilled counts. Tasks solved in study mode and the date each objective was first studied and first completed are in study-<track>.json, next to the history (it survives ./teardown.sh the same way); study results never reach the statistics, the weak list or the exam gate. ./exam end says which objectives a session got over a line and what comes next.

The browser UI has the same thing in the lobby: the ladder, Continue the path, and Study an objective (the full list with notes and progress). During a study session, and in a drill once a task has been graded, the notes of the task's objective sit under its reference solution.

CKA_STUDY_GUIDE.md and CKAD_STUDY_GUIDE.md are the notes of every objective as one document, generated with ./exam --track <track> notes all --md.

CKAD track

./exam --track ckad ... (every command takes it), on its own Kubernetes v1.37 cluster (./setup.sh ckad). A mock draws 16 tasks in 120 minutes, weighted like the exam: Design and Build 3, Deployment 3, Observability 3, Environment and Security 4, Networking 3, from a pool of 44 (9/8/11/11/5), so no two mocks are the same set. Every line of the curriculum has at least two pool tasks: image builds, Jobs, DaemonSets, native sidecars, volumes; blue/green, canary, rolling-update limits, Helm, Kustomize; API deprecations, probes, kubectl top, logs, debugging (CrashLoopBackOff, OOMKilled, config errors); CRDs, RBAC, Pod Security admission, quotas and LimitRanges, QoS, ConfigMaps, Secrets, ServiceAccounts; NetworkPolicy (tested with real traffic), Services, Ingress with TLS.

A few tasks need more than kubectl on this machine: podman (Q13, Q41 and Q60 build images), helm (Q6, Q21, Q43), and network access the first time Q23 runs on a fresh cluster (it installs metrics-server). Drill a domain with ./exam --track ckad drill ENV, as for CKA, or an objective with ./exam --track ckad drill 4.3.

Simulator Set A (./exam --track ckad sim a): 20 original killer.sh-style tasks, 140 points, 120 minutes, exam rules - a fixed set (ids 38-57) kept out of the random pool, so reruns are comparable. Riverline, a fictional river-freight company, one Namespace per river. Expect several steps per task, exact names and file outputs, and a few things the pool never asks: debugging a container with no shell through an ephemeral container, a Kustomize patch with a hashed configMapGenerator, a Helm release stuck in failed, fsGroup on a shared volume, and the two-source NetworkPolicy trap.

Scoring, history, drills

Each task is a list of named, weighted checks (tracks/cka/questions.py). You get the points for every check that passes, so a task can score 4/6 — the same partial-credit model as the exam. Every submit appends to history-<track>.jsonl and regenerates weak-spots-<track>.md: latest domain scores, a "drill next" section listing the failing checks per task, and a ✓ / n-of-m matrix across all attempts.

A task counts as weak if it failed on its latest attempt, or has failed at least twice and has not yet been passed twice in a row since, or was never attempted (fixed-set tasks excepted — note that this makes an early drill weak very large; name tasks explicitly for a short drill). Drills give 6 minutes per task and — unlike exam mode — let you check a task while you work. History entries marked "synthetic": true (scripted verification runs, not a person) are kept in the file but left out of every statistic.

Using your own cluster instead of kind

Any kubeadm-style cluster with 1 control-plane + 3 workers works. Skip setup.sh, set (Fish) — these override the cluster of every track, so use one on the minor of the track you practise (session start warns when the version differs):

set -x CKA_CONTEXT     kubernetes-admin@kubernetes
set -x CKA_NODE_ACCESS ssh        # podman | docker | ssh | vagrant
set -x CKA_CP cp1;  set -x CKA_W1 node1;  set -x CKA_W2 node2;  set -x CKA_W3 node3
./exam web

ssh needs passwordless ssh <node-name> (for Vagrant: vagrant ssh-config >> ~/.ssh/config); vagrant runs vagrant ssh from the Vagrantfile directory. Tasks 2 and 20 write to /opt/cka and /etc/kubernetes/manifests, so the login must be able to. Managed clusters (EKS/AKS/GKE) won't work — no node or control-plane access. Startup preflights all of this.

The etcd tasks (2 and 45) need etcdctl/etcdutl on the control-plane node, as the real exam has them — they cannot be copied out of the running pod, since the etcd image is distroless. setup.sh installs them on kind; on any cluster, seeding those tasks installs them if they are missing (from the etcd image in the node's containerd, else the upstream release, else apt) and fails loudly if it cannot. ./etcd-tools.sh [track] does the same by hand. The CKA cluster runs etcd 3.6, as the exam does: etcdutl restores and inspects snapshots, etcdctl only saves them.

CKA bank

Domain Normal (54) Hard (24)
Architecture RBAC, etcd snapshot, Kustomize, CRD, static pod, certs/CSR, Helm, aggregated ClusterRole, kubectl explain on a CRD, kubeadm join token, component versions, kernel prerequisites of a node, the runtime behind a Pod (crictl), draining the control-plane node, the join of a second control-plane node, leader leases + etcd members, CNI/CSI/CRI inventory exact RBAC with an over-grant to remove, etcd restore, static pod on a relocated path, user → self-contained kubeconfig, Kustomize generators + patches, CRD validation/defaults, renew all control-plane certs
Workloads rolling update, sidecar, node affinity + resources, ConfigMap/Secret, metrics-server repair + HPA, PriorityClass, taints, DaemonSet, CronJob, init container, immutable ConfigMap rotation, parallel Job, quota + LimitRange, HPA with a scale-down policy, probes that check the wrong thing anti-affinity + toleration + priority under a quota, rollback to a specific revision then pause, native sidecar + probes + exact report, cluster audit to exact files, work in the context's namespace
Networking NetworkPolicy, NodePort, Ingress, cluster DNS, Gateway API, named ports, egress-only policy, headless + ExternalName, Pod-to-Pod across nodes zero-trust namespace (4 policies + trap), TLS Ingress with generated cert + fingerprint, Gateway canary with header match and weights, DNS down three ways
Storage PV/PVC/pod, default StorageClass, re-bind a Released PV, data survives the pod bind the right local volume by selector, migrate data between claims via a Job
Troubleshooting node NotReady, broken deployment + service, CrashLoopBackOff, scheduler down, CoreDNS misconfig, controller-manager down, kubelet config drift, kubectl top, logs by selector and of an init container worker with three faults (+ uncordon check), two control-plane components down, app down for four reasons fixed in place (UID check), cluster facts exactly

Disruptive tasks (anything that breaks a node or control-plane component) are seeded last and can block other tasks until fixed — read everything first, as in the real exam. Tasks that damage or depend on the same shared state declare it in breaks and never share a session, and the etcd restore (Q45) is solo: restoring rolls the whole cluster back, so it only runs on its own. Before every session a baseline pass repairs anything a previous, unfinished session left broken.

The kindnet CNI here enforces NetworkPolicy (it embeds a policy engine), so a wrong policy really drops traffic; no ingress or Gateway controller runs, so those are graded on the objects.

Simulator sets (./exam sim c|d, never drawn into random sessions): C is modelled on an 18-question commercial mock sitting, and D is killer.sh-style — control-plane inventory, event forensics with crictl, a broken kubelet unit, kubelet certificates, manual scheduling with the scheduler stopped, Helm housekeeping with a stuck release, and an enforced AND-vs-OR NetworkPolicy. Both are original scenarios, not copies of any commercial simulator. (Sets A and B were retired in favour of these two; their last versions are in git history.)

Adding tasks or a new track

Banks live in tracks/cka/{questions,bank_extra,bank_hard,bank_objectives,bank_sim_*}.py; a task is:

{"id": 114, "domain": "NET", "difficulty": "normal",   # or "hard" - ids stay contiguous
 "params": lambda r: {"ns": ns_pick(r, "svc"), "port": r.choice([8080, 9090])},
 "title": "...", "task": "...markdown; %{ns} and %{port} are substituted; `code` copies on click...",
 "setup": "bash that seeds the scenario (may use %{ns})",
 "checks": [C("what this check proves", 2, "bash; exit 0 = pass"), ...],
 "disruptive": False,          # True: seeded last, after everything else is scheduled
 "breaks": ["w1-taint"],       # shared state it damages or needs intact; never paired with a task sharing any
 "solo": False}                # True: only ever drilled on its own

Scripts get $K (pinned to the grading context), $WS, $CP/$W1/$W2/$W3, nx NODE 'cmd', and helpers j <get args> '{jsonpath}' and eq a b. Task text may use {WS}, {CP}, {W2}, {W3}, {SHELL_CP} etc. TRACK["quota"] / ["quota_hard"] set how many tasks per domain a session draws, TRACK["k8s"] the Kubernetes minor (and so the cluster) the track runs on, and TRACK["exam_contexts"] whether it owns the k8s-c* contexts. For CKS, add to QUESTIONS in its track; TRACK already carries the domain table and pass mark.

In a track with a curriculum.py, list a new task under the objective(s) it practises ("tasks"). ./exam objectives warns about a task that no objective lists, a listed id that does not exist, and an objective left with fewer than two pool tasks. A track without that file simply has no objective commands (CKS today).