287 lines
19 KiB
Markdown
287 lines
19 KiB
Markdown
# 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](#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](#study-path-the-curriculum-objective-by-objective)).
|
|
|
|
## 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:
|
|
|
|
```python
|
|
{"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).
|