# CKA Mock Exam Pack v1 — SOLUTIONS > ⚠️ **Do not open before a timed attempt.** killer.sh rules: solve first, autopsy second. Reading solutions cold converts a diagnostic into trivia. > Each solution: the fastest correct path → why it works → the planted trap → what a grader validates. --- ## Q1 — NotReady node (8%) ```bash kubectl get nodes # drills-worker2 NotReady docker exec -it drills-worker2 bash # real exam: ssh node systemctl status kubelet # inactive (dead) systemctl start kubelet && systemctl enable kubelet exit kubectl get node drills-worker2 # Ready within ~30s mkdir -p /tmp/exam && echo kubelet > /tmp/exam/q1-component.txt ``` **Why:** NotReady = node agent not reporting. Diagnostic ladder on the node: `systemctl status kubelet` → if running, `journalctl -u kubelet -f` for cert/config/CNI errors → container runtime (`systemctl status containerd`). Here it's simply stopped — the most common exam variant. **Trap:** none beyond forgetting `enable` (real exam expects the fix to survive reboot; graders have failed people on start-without-enable). **Grader checks:** node Ready + the component name in the file. --- ## Q2 — etcd snapshot (8%) ```bash docker exec -it drills-control-plane bash ETCDCTL_API=3 etcdctl snapshot save /root/etcd-backup.db \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key etcdctl snapshot status /root/etcd-backup.db -w table exit docker exec drills-control-plane etcdctl snapshot status /root/etcd-backup.db -w table > /tmp/exam/q2-status.txt ``` If `etcdctl` isn't on the node's PATH, run it inside the etcd pod instead: `kubectl -n kube-system exec etcd-drills-control-plane -- sh -c 'ETCDCTL_API=3 etcdctl snapshot save ...'` — but then the file lands in the pod's filesystem; since etcd's static pod hostPath-mounts `/etc/kubernetes/pki/etcd`, save to a hostPath-mounted dir or copy out. Node-local etcdctl is cleaner when present. **Why those flags:** etcd serves TLS with client-cert auth; the cert paths are the kubeadm defaults — you don't memorize them, you read them live: `grep -E 'cert|key|ca' /etc/kubernetes/manifests/etcd.yaml`. That grep is the actual skill. **Trap:** using `--cert=.../apiserver-etcd-client.crt` also works (it's a valid client cert) — but pointing at the *apiserver's serving* cert doesn't. When in doubt, read etcd.yaml's own `--cert-file/--key-file` lines. **Grader checks:** file exists on the CP node, `snapshot status` output captured. Real exam restore variant: `etcdctl snapshot restore --data-dir=/var/lib/etcd-restore`, then edit etcd.yaml's hostPath to the new dir; kubelet restarts the static pod. --- ## Q3 — NetworkPolicy (6%) ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: backend-shield namespace: ex-pluto spec: podSelector: matchLabels: {role: backend} policyTypes: [Ingress] # Ingress only — egress stays open per task ingress: - from: - podSelector: matchLabels: {role: frontend} ports: - {protocol: TCP, port: 80} ``` **Why:** listing only `Ingress` in policyTypes restricts ingress and leaves egress untouched — adding `Egress` with no egress rules would silently deny all outbound, violating the task. `from.podSelector` without a namespaceSelector = same-namespace pods only, which is exactly the requirement. **Trap:** cache pod has a label too (`role=cache`) — but selection is allowlist-based: not matching `from` = denied. The drill is trusting deny-by-default once any policy selects the pod. **Grader checks:** functional — curl from frontend succeeds, from cache times out. Always run both directions yourself; a policy selecting zero pods passes the positive test and fails the negative one. --- ## Q4 — HTTPRoute with header exception (7%) ```yaml apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: release-route namespace: ex-venus spec: parentRefs: - name: main-gate rules: - matches: # specific rule first: path AND header - path: {type: PathPrefix, value: /} headers: - {name: X-Version, type: Exact, value: v2} backendRefs: - {name: web-v2, port: 5678} - matches: # catch-all - path: {type: PathPrefix, value: /} backendRefs: - {name: web-v1, port: 5678} ``` **Why:** conditions inside ONE match entry are ANDed (path ∧ header); separate entries in the matches list are ORed. The header rule must be its own rule with both conditions co-located. Spec precedence already prefers more-specific matches (header count breaks path-length ties), but ordering specific-first documents intent and defends against lax implementations. **Trap:** putting header and path in two match entries → OR → every request matches the "v2" rule. **Grader checks:** parentRef=main-gate, AND-structure, both backends with correct ports. --- ## Q5 — two-fault deployment (7%) ```bash kubectl -n ex-neptune describe pod -l app=web-portal # fault A: ImagePullBackOff nginx:1.99-fake # fault B: CreateContainerConfigError — configmap "portal-config" not found kubectl -n ex-neptune create cm portal-config --from-literal=MODE=production kubectl -n ex-neptune set image deploy/web-portal nginx-1-99-fake=nginx:1.27 # (container name from: kubectl -n ex-neptune get deploy web-portal -o jsonpath='{.spec.template.spec.containers[0].name}') kubectl -n ex-neptune rollout status deploy/web-portal # 2/2 ``` **Why:** the task says two *independent* faults — fixing one and declaring victory is the trap. Image fix alone → pods still stuck on the missing CM; CM alone → still ImagePullBackOff. `describe` shows both symptoms at once if you read the whole Events section. **Trap:** the container is named after the bogus image by the generator — `set image deploy/web-portal *=nginx:1.27` (wildcard) sidesteps needing the name. **Grader checks:** 2/2 Ready + CM exists with MODE=production + envFrom intact. --- ## Q6 — Service without endpoints (6%) ```bash kubectl -n ex-mars get endpoints api-svc # kubectl -n ex-mars get svc api-svc -o yaml | grep -A2 selector # app: api-backent ← typo kubectl -n ex-mars get pods --show-labels # pods carry app=api-backend kubectl -n ex-mars patch svc api-svc -p '{"spec":{"selector":{"app":"api-backend"}}}' kubectl -n ex-mars get endpoints api-svc # two IPs ``` **Why:** empty Endpoints with healthy pods = selector/label mismatch ~90% of the time (the rest: no pods Ready, or wrong targetPort). The diff-the-strings discipline (`api-backent` vs `api-backend`) is the whole question. **Trap:** task says fix *the service* — relabeling the pods "works" functionally but violates the instruction and may fail the check. **Grader checks:** Endpoints populated; deployment untouched. --- ## Q7 — imperative RBAC (7%) ```bash kubectl -n ex-saturn create role deploy-manager \ --verb=create --verb=list --verb=delete --resource=deployments kubectl -n ex-saturn create rolebinding deploy-bot-binding \ --role=deploy-manager --serviceaccount=ex-saturn:deploy-bot kubectl auth can-i create deployments --as=system:serviceaccount:ex-saturn:deploy-bot -n ex-saturn > /tmp/exam/q7-cani.txt # yes kubectl auth can-i create pods --as=system:serviceaccount:ex-saturn:deploy-bot -n ex-saturn >> /tmp/exam/q7-cani.txt # no ``` **Why:** Role (not ClusterRole) = namespace-scoped as demanded; `--serviceaccount=ns:name` is the binding syntax people forget (NOT `--user`). Identity string for can-i: `system:serviceaccount::`. **Trap:** "nothing else" — resist adding get/watch out of habit; graders sometimes diff the verb list. **Grader checks:** the two can-i outcomes, role shape. --- ## Q8 — Helm pin → upgrade keeping values (6%) ```bash helm search repo bitnami/redis --versions | head -5 # note latest (e.g. 21.x.y) and one minor back (21.(x-1).z or 20.x) helm install cache-layer bitnami/redis -n ex-titan \ --version \ --set architecture=standalone --set auth.enabled=false helm upgrade cache-layer bitnami/redis -n ex-titan \ --version --reuse-values helm get values cache-layer -n ex-titan # both overrides intact helm history cache-layer -n ex-titan > /tmp/exam/q8-history.txt ``` **Why:** `--reuse-values` is the "keep my overrides" flag — the planted trap. Bare `helm upgrade` resets to chart defaults + whatever `--set` you pass *now*; people assume values persist. (Equally valid: repeat both `--set` flags on upgrade — explicit beats clever; `--reuse-values` has sharp edges when combined with new `--set`s, merging old+new.) **Grader checks:** history shows 2 revisions, deployed chart = latest, values retain both overrides. --- ## Q9 — Kustomize overlay (4%) `/tmp/drill-kz/overlays/exam/kustomization.yaml`: ```yaml resources: [../../base] namespace: ex-titan namePrefix: exam- images: - name: nginx newTag: "1.27" ``` ```bash kubectl kustomize /tmp/drill-kz/overlays/exam # eyeball first — always render before apply kubectl apply -k /tmp/drill-kz/overlays/exam ``` **Why:** the `images` transformer rewrites by image *name* regardless of the tag in base — no patch file needed for a tag change. `namePrefix` renames the deployment to `exam-portal` (validation greps that name — a hint hidden in the pack). **Trap:** writing a strategicMerge patch for something transformers do in 3 lines — works, but slow. --- ## Q10 — HPA needing requests (5%) ```bash kubectl -n ex-io set resources deploy metrics-writer --requests=cpu=100m # the hidden half kubectl -n ex-io autoscale deploy metrics-writer --name=writer-hpa \ --min=1 --max=4 --cpu-percent=70 --dry-run=client -o yaml > /tmp/hpa.yaml # edit: ensure autoscaling/v2 shape, add: # behavior: # scaleDown: # stabilizationWindowSeconds: 240 kubectl apply -f /tmp/hpa.yaml ``` Final spec core: ```yaml apiVersion: autoscaling/v2 spec: scaleTargetRef: {apiVersion: apps/v1, kind: Deployment, name: metrics-writer} minReplicas: 1 maxReplicas: 4 metrics: - type: Resource resource: {name: cpu, target: {type: Utilization, averageUtilization: 70}} behavior: scaleDown: stabilizationWindowSeconds: 240 ``` **Why:** Utilization = percentage *of requests* — no requests, no math, TARGETS shows `` forever. The task text told you ("part of the task is making the HPA functional"). `behavior` requires autoscaling/v2. **Grader checks:** the 70 + 240 fields AND cpu requests present on the deployment. --- ## Q11 — rollback (5%) ```bash kubectl -n ex-mercury rollout history deploy/release-app # rev1 1.25, rev2 1.26, rev3 1.27-bogus (stuck) kubectl -n ex-mercury rollout undo deploy/release-app # → back to rev2 content (1.26), recorded as rev4 kubectl -n ex-mercury rollout status deploy/release-app kubectl -n ex-mercury get deploy release-app -o jsonpath='{.spec.template.spec.containers[0].image}' # nginx:1.26 { echo "image: nginx:1.26"; echo "rolled back to content of revision 2 (now revision 4)"; } > /tmp/exam/q11-rollback.txt ``` **Why:** `undo` without `--to-revision` targets the last *fully deployed* revision — here 1.26. Note the revision numbering quirk: the rollback re-creates the old template as a NEW revision number; "revision you rolled back to" means the source revision (2). **Trap:** `--to-revision=1` overshoots to 1.25 — "previous working" is 1.26. `rollout history --revision=2` shows the template if unsure. **Grader checks:** image 1.26, deployment Available, file present. --- ## Q12 — taint + affinity combo (5%) ```bash kubectl -n ex-io create deploy edge-daemon --image=busybox --replicas=2 \ --dry-run=client -o yaml -- sleep 3600 > /tmp/ed.yaml ``` Add to pod template spec: ```yaml tolerations: - {key: tier, operator: Equal, value: critical, effect: NoSchedule} affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - {key: zone, operator: In, values: [east]} ``` **Why both:** a toleration only *permits* landing on the tainted node — it doesn't attract; the scheduler could still place pods on worker2. Affinity only *restricts* to worker — but without the toleration the taint repels them and pods go Pending. Permission + constraint together = deterministic placement. (This is the one-sentence answer the drill-pack twin of this task demands.) **Grader checks:** both pods on drills-worker via `-o wide`. --- ## Q13 — WaitForFirstConsumer (5%) ```yaml apiVersion: v1 kind: PersistentVolume metadata: {name: vol-alpha} spec: capacity: {storage: 2Gi} accessModes: [ReadWriteOnce] storageClassName: local-manual hostPath: {path: /opt/vol-alpha} --- apiVersion: v1 kind: PersistentVolumeClaim metadata: {name: claim-alpha, namespace: ex-io} spec: accessModes: [ReadWriteOnce] storageClassName: local-manual resources: {requests: {storage: 1Gi}} ``` ```bash echo "SC uses volumeBindingMode: WaitForFirstConsumer — binding deferred until a pod schedules, so scheduler can pick topology-compatible PV" > /tmp/exam/q13-why.txt kubectl -n ex-io run vol-user --image=nginx --dry-run=client -o yaml > /tmp/vu.yaml # add: volumes: [{name: d, persistentVolumeClaim: {claimName: claim-alpha}}] # volumeMounts: [{name: d, mountPath: /data}] kubectl apply -f /tmp/vu.yaml kubectl -n ex-io get pvc claim-alpha # Bound after pod schedules ``` **Why:** WaitForFirstConsumer defers bind so node placement can inform PV choice — Pending-before-pod is *designed behavior*, not a fault. Also note 1Gi request binds to a 2Gi PV: binding requires PV ≥ request, not equality. **Grader checks:** the explanation file, Bound state, working mount. --- ## Q14 — default StorageClass (5%) ```bash kubectl patch sc local-manual -p \ '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}' kubectl apply -f - <<'EOF' apiVersion: v1 kind: PersistentVolumeClaim metadata: {name: claim-default, namespace: ex-io} spec: accessModes: [ReadWriteOnce] resources: {requests: {storage: 100Mi}} EOF kubectl -n ex-io get pvc claim-default -o jsonpath='{.spec.storageClassName}' > /tmp/exam/q14-proof.txt # local-manual ``` **Why:** the default-class annotation makes the admission plugin inject the class into class-less PVCs *at creation time*. Exact annotation string matters — it's on `kubernetes.io/docs/tasks/administer-cluster/change-default-storage-class/`. **Trap:** kind ships `standard` (local-path) as default — real exam variant often requires *demoting* the old default first (`is-default-class: "false"`), since two defaults make behavior version-dependent. Do both to be safe. **Grader checks:** annotation true, PVC auto-assigned the class. --- ## Q15 — Ingress generator (5%) ```bash kubectl -n ex-mars create ingress api-ingress \ --class=nginx \ --rule="api.exam.local/v1*=api-svc:80" kubectl -n ex-mars get ingress api-ingress -o jsonpath='{.spec.rules[0].http.paths[0].pathType}' # Prefix ``` **Why:** the trailing `*` in the rule path is what emits `pathType: Prefix`; bare `/v1` → `Exact` → fails the explicit requirement. `--class` sets `ingressClassName`. Whole task is one generator line — hand-writing this YAML is the time-loss trap. **Grader checks:** host, path, pathType Prefix, class, backend svc:port. --- ## Q16 — crashing pod: capture then fix (6%) ```bash kubectl -n ex-rescue logs data-proc > /tmp/exam/q16-logs.txt # works after termination — logs persist kubectl -n ex-rescue delete pod data-proc $now kubectl -n ex-rescue run data-proc --image=busybox --restart=Never -- sleep 3600 kubectl -n ex-rescue get pod data-proc # Running ``` **Why:** ORDER is the trap — capture logs BEFORE delete; deleting first destroys the evidence and the grader's grep for the FATAL line fails. `kubectl logs` works on Failed pods (container logs persist until pod object removal). If a pod is crash-looping (restarting), `logs --previous` gets the prior attempt. **Grader checks:** FATAL line in the file + same-name pod Running. --- ## Q17 — broken static pod (5%) ```bash docker exec -it drills-worker bash cat /etc/kubernetes/manifests/edge-cache.yaml # image: redis:7-alpinee ← typo sed -i 's/alpinee/alpine/' /etc/kubernetes/manifests/edge-cache.yaml exit kubectl get pod edge-cache-drills-worker # Running (mirror pod = name + node suffix) ``` **Why:** static pods are kubelet-local — the file in `staticPodPath` IS the source of truth; editing it makes kubelet recreate the pod (no apply, no API involvement). The mirror pod visible in kubectl is read-only — deleting it via kubectl just respawns it; fixes happen on the node. **Trap:** hunting for a deployment/controller that doesn't exist. "Configured on the node + invisible/broken in the API" should scream *static pod* — go read the manifests dir. Also: with ImagePullBackOff the mirror pod may actually be visible-but-broken; either way the fix path is identical. **Grader checks:** mirror pod Running in default ns. --- ## Meta-lessons across the paper 1. **Every troubleshooting Q was diagnosable from `describe`/Events/logs in <60s** — the fix is trivial once the read is right. Budget reading time, not typing time. 2. **Generators + patch covered 10 of 17 tasks** with zero hand-written YAML from scratch. 3. **Traps cluster around order-of-operations** (logs before delete, demote old default, capture before fix) and **silent partial success** (one of two faults fixed, values lost on upgrade, OR instead of AND). The verification lap exists to catch exactly these.