18 KiB
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%)
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%)
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%)
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%)
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%)
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%)
kubectl -n ex-mars get endpoints api-svc # <none>
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%)
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:<ns>:<name>.
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%)
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 <one-minor-back> \
--set architecture=standalone --set auth.enabled=false
helm upgrade cache-layer bitnami/redis -n ex-titan \
--version <latest> --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 --sets, 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:
resources: [../../base]
namespace: ex-titan
namePrefix: exam-
images:
- name: nginx
newTag: "1.27"
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%)
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:
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 <unknown> 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%)
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%)
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:
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%)
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}}
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%)
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%)
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%)
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%)
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
- 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. - Generators + patch covered 10 of 17 tasks with zero hand-written YAML from scratch.
- 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.