16 KiB
CKA Mock Exam Pack v2 — SOLUTIONS
⚠️ Post-attempt autopsy only. Fastest path → why → trap → grader focus.
Q1 — dead scheduler (8%)
kubectl -n kube-system get pods | grep scheduler # missing or CrashLoop
docker exec -it drills-control-plane bash
crictl ps -a | grep sched # nothing healthy
grep command -A3 /etc/kubernetes/manifests/kube-scheduler.yaml # kube-schedulerr ← typo
sed -i 's|kube-schedulerr|kube-scheduler|' /etc/kubernetes/manifests/kube-scheduler.yaml
exit
kubectl -n kube-system get pods | grep scheduler # Running
echo kube-scheduler > /tmp/exam2/q1.txt
Why: Pending + zero events = nothing is making scheduling decisions → scheduler. Static-pod manifests on the CP are the first read; kubelet auto-restarts on file change. Trap/meta: fix this FIRST — Q4, Q13, Q14 and every new pod depend on it. Triage was the real test. Grader: scheduler pod healthy, new pods schedule.
Q2 — CoreDNS Corefile (7%)
kubectl -n kube-system logs deploy/coredns | tail # unknown directive "bogusplugin"
kubectl -n kube-system edit cm coredns # restore a valid Corefile:
.:53 {
errors
health { lameduck 5s }
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf { max_concurrent 1000 }
cache 30
loop
reload
loadbalance
}
kubectl -n kube-system rollout restart deploy coredns
echo "invalid plugin directive in Corefile broke coredns" > /tmp/exam2/q2.txt
Why: crash-looping CoreDNS after a config change = read its logs; it names the bad directive. The sabotage also dropped the kubernetes plugin — without it cluster names never resolve even if pods run. The canonical Corefile is on kubernetes.io/docs/tasks/administer-cluster/dns-custom-nameservers/.
Trap: restarting pods without fixing the CM; or fixing syntax but not restoring the kubernetes block.
Grader: nslookup kubernetes.default succeeds.
Q3 — broken kubeconfig (6%)
kubectl --kubeconfig /tmp/exam2/developer.kubeconfig get nodes # connection refused :PORT
grep server /tmp/exam2/developer.kubeconfig
grep server ~/.kube/config # compare — port off by one
sed -i 's|:WRONGPORT|:RIGHTPORT|' /tmp/exam2/developer.kubeconfig
echo "server URL had wrong apiserver port" > /tmp/exam2/q3.txt
Why: kubeconfig triage = three suspects: server URL (connection refused/timeout), CA data (x509 errors), client creds (401/403). Refused ⇒ URL. Diff against a working config instead of guessing. Grader: the command works using that file; admin config untouched.
Q4 — PDB-blocked drain (5%)
kubectl drain drills-worker2 --ignore-daemonsets --delete-emptydir-data # evictions blocked by pinned-pdb
kubectl -n ex2-neptune get pdb pinned-pdb # maxUnavailable: 0 — nothing may ever be evicted
kubectl -n ex2-neptune patch pdb pinned-pdb --type merge -p '{"spec":{"maxUnavailable":1}}'
kubectl -n ex2-neptune patch deploy pinned --type=json -p='[{"op":"remove","path":"/spec/template/spec/nodeSelector"}]'
kubectl drain drills-worker2 --ignore-daemonsets --delete-emptydir-data # proceeds
kubectl uncordon drills-worker2
echo "PDB maxUnavailable:0 blocked eviction" > /tmp/exam2/q4.txt
Why: maxUnavailable: 0 makes every eviction violate the budget — drain retries forever. maxUnavailable: 1 (or minAvailable: 1 on 2 replicas) is the minimal loosening that keeps ≥1 alive. Second wrinkle: the deploy is nodeSelector-pinned to the draining node — evicted pods can't reschedule elsewhere and would wedge availability; dropping the selector (or tolerating pending-until-uncordon, defensible if argued) completes it.
Trap: deleting the PDB = not "minimal + still protective"; graders check the PDB still exists.
Q5 — readiness probe (4%)
kubectl -n ex2-front describe pod -l app=shop-ui | grep -A3 Readiness # httpGet :8080 — nginx listens on 80
kubectl -n ex2-front patch deploy shop-ui --type=json \
-p='[{"op":"replace","path":"/spec/template/spec/containers/0/readinessProbe/httpGet/port","value":80}]'
Why: Running-but-not-Ready ⇒ probe. describe shows probe target and failure events; nginx serves :80.
Grader: 2/2 READY.
Q6 — etcd backup AND restore (8%)
docker exec -it drills-control-plane bash
ETCDCTL_API=3 etcdctl snapshot save /root/etcd-v2.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
exit
kubectl create cm marker -n default --from-literal=state=after-backup
docker exec -it drills-control-plane bash
ETCDCTL_API=3 etcdutl snapshot restore /root/etcd-v2.db --data-dir /var/lib/etcd-restore # etcdctl also works
sed -i 's|path: /var/lib/etcd$|path: /var/lib/etcd-restore|' /etc/kubernetes/manifests/etcd.yaml
# kubelet notices manifest change, restarts etcd on the restored dir; apiserver reconnects (~30-60s)
exit
kubectl get cm marker -n default # NotFound — restore proven
Why: restore never touches the live data dir — unpack the snapshot to a NEW dir, repoint the static pod's hostPath. The marker CM created after the snapshot must vanish: that's the only honest proof a restore happened.
Trap: editing --data-dir flag but not the volumes hostPath (or vice versa) — the hostPath mount is what matters since the container path stays /var/lib/etcd... check BOTH lines in etcd.yaml; safest is changing the hostPath only. Expect a scary minute of apiserver flapping — that's normal.
Grader: marker gone, snapshot file present, cluster healthy.
Q7 — broken chart (6%)
helm lint /tmp/exam2/shipper
# fault 1: Chart.yaml apiVersion "v3" invalid → v2
sed -i 's/^apiVersion: v3/apiVersion: v2/' /tmp/exam2/shipper/Chart.yaml
helm template /tmp/exam2/shipper
# fault 2: deployment.yaml — unclosed action {{ .Values.replicaCount }
sed -i 's|{{ .Values.replicaCount }$|{{ .Values.replicaCount }}|' /tmp/exam2/shipper/templates/deployment.yaml
helm install shipper /tmp/exam2/shipper -n ex2-batch --set replicaCount=2
Why: the debug ladder is lint (metadata/structure) → template (render/syntax) → install --dry-run (cluster validation). lint catches Chart.yaml; only template rendering exposes the brace fault — one tool doesn't see both, which is the lesson.
Grader: release deployed, 2 replicas via CLI override.
Q8 — ValidatingAdmissionPolicy (6%)
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata: {name: require-owner}
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["deployments"]
validations:
- expression: "has(object.metadata.labels) && 'owner' in object.metadata.labels"
message: "deployment must carry an 'owner' label"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata: {name: require-owner-binding}
spec:
policyName: require-owner
validationActions: [Deny]
matchResources:
namespaceSelector:
matchLabels: {env: guarded}
Prove:
kubectl -n ex2-guard create deploy bad --image=nginx # denied, message shown
kubectl -n ex2-guard create deploy good --image=nginx --dry-run=client -o yaml \
| kubectl label --local -f - owner=me -o yaml | kubectl apply -f - # accepted
Why: VAP = CEL expression evaluated in-apiserver, no webhook infra. Policy defines the rule; the binding scopes it (namespaceSelector) and sets the action — forgetting the binding = policy silently inert. has() guard first: CEL errors on absent maps otherwise.
Trap: validation block in the exam paper creates-then-labels — that ORDER gets denied; the label must exist at CREATE. Spotting that is part of the task.
Grader: deny with "owner" in message; labeled create passes.
Docs: kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/.
Q9 — kustomize from scratch (5%)
/tmp/exam2/kz/deploy.yaml: plain nginx:1.27 deployment board (1 replica) with envFrom: [{configMapRef: {name: board-cfg}}].
/tmp/exam2/kz/replicas.yaml:
- op: replace
path: /spec/replicas
value: 3
/tmp/exam2/kz/kustomization.yaml:
namespace: ex2-batch
resources: [deploy.yaml]
configMapGenerator:
- name: board-cfg
literals: [MODE=exam]
patches:
- path: replicas.yaml
target: {kind: Deployment, name: board}
kubectl apply -k /tmp/exam2/kz
Why: configMapGenerator emits board-cfg-<hash> AND rewrites every reference to it — that's why the deployment references the plain name and kustomize wires the suffix. JSON6902 patch = op/path/value list with a target selector.
Trap: creating the CM manually with the literal name — then the generated/hash mechanics the task demands never happen.
Grader: replicas 3, hashed CM present, envFrom points at hashed name.
Q10 — weighted split + ReferenceGrant (8%)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: {name: split-route, namespace: ex2-web}
spec:
parentRefs: [{name: shop-gate}]
rules:
- matches: [{path: {type: PathPrefix, value: /shop}}]
backendRefs:
- {name: web-v1, port: 5678, weight: 90}
- name: web-v2
namespace: ex2-canary
port: 5678
weight: 10
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata: {name: allow-web-routes, namespace: ex2-canary} # lives in the TARGET ns
spec:
from:
- {group: gateway.networking.k8s.io, kind: HTTPRoute, namespace: ex2-web}
to:
- {group: "", kind: Service}
Why: weights on sibling backendRefs in ONE rule = traffic split (proportions of summed weights). Cross-namespace backendRefs are denied by default — ReferenceGrant is consent, and it lives in the namespace being referenced (the target grants, the referrer can't self-authorize). That direction is the entire question. Trap: grant in ex2-web (wrong side); or two separate rules instead of two weighted backendRefs (that's not a split, first match wins). Grader: weights 90/10, namespace on v2 ref, grant in ex2-canary with correct from/to.
Q11 — egress netpol + DNS (7%)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: app-egress, namespace: ex2-data}
spec:
podSelector: {matchLabels: {role: app}}
policyTypes: [Egress]
egress:
- to: [{podSelector: {matchLabels: {role: db}}}]
ports: [{protocol: TCP, port: 80}]
- ports:
- {protocol: UDP, port: 53}
- {protocol: TCP, port: 53}
Why: once Egress is a policyType, everything outbound not allowlisted dies — including DNS to kube-system. The bare-ports rule (no to) = "port 53 to anywhere", the standard DNS carve-out. Without it, even curl $DBIP by IP works but anything by name fails — the classic silent egress-policy footgun.
Trap: forgetting DNS; or scoping the db rule with a namespaceSelector it doesn't need (same-ns podSelector suffices).
Grader: app→db 200, app→rogue timeout.
Q12 — NodePort (5%)
kubectl -n ex2-web expose deploy web-v1 --name=web-np --port=5678 --type=NodePort \
--dry-run=client -o yaml > /tmp/np.yaml
# add under ports[0]: nodePort: 30080 (expose can't set it)
kubectl apply -f /tmp/np.yaml
docker exec drills-worker curl -s -m 2 localhost:30080 # v1
Why: the generator can't pin nodePort — generate, add one field, apply. NodePort listens on every node regardless of pod placement (kube-proxy routes) — hence curl works from any node. Grader: fixed 30080, in-range (30000-32767), reachable.
Q13 — spread + priority (6%)
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata: {name: exam-critical}
value: 100000
globalDefault: false
Deployment adds:
priorityClassName: exam-critical
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector: {matchLabels: {app: spread-app}}
Why: PriorityClass is cluster-scoped, referenced by name in the pod spec. The spread constraint's own labelSelector is mandatory and must match the pods (self-selection) — its absence makes the constraint a no-op, the classic miss. CP taint keeps pods off the control-plane, so 4 replicas ⇒ 2+2 across workers with maxSkew 1. Grader: 2/2 split, both fields present.
Q14 — DaemonSet incl. CP (5%)
kubectl -n ex2-batch create deploy node-agent --image=busybox --dry-run=client -o yaml -- sleep 3600 > /tmp/ds.yaml
# edit: kind: DaemonSet, delete replicas+strategy, add toleration:
# tolerations: [{key: node-role.kubernetes.io/control-plane, operator: Exists, effect: NoSchedule}]
kubectl apply -f /tmp/ds.yaml
Why: no DS generator exists — mutate a deploy skeleton (delete replicas, strategy; change kind). DS schedules per-node automatically; only the CP taint stands between you and 3/3, hence the toleration.
Grader: DESIRED=READY=3, one pod on drills-control-plane.
Q15 — native sidecar (4%)
Pod template:
volumes: [{name: logs, emptyDir: {}}]
initContainers:
- name: shipper
image: busybox
restartPolicy: Always # ← makes it a sidecar
command: ["sh","-c","touch /var/log/audit/audit.log; tail -f /var/log/audit/audit.log"]
volumeMounts: [{name: logs, mountPath: /var/log/audit}]
containers:
- name: app
image: busybox
command: ["sh","-c","while true; do date >> /var/log/audit/audit.log; sleep 5; done"]
volumeMounts: [{name: logs, mountPath: /var/log/audit}]
Why: restartPolicy: Always on an initContainer = native sidecar — starts before app, doesn't block pod completion, restarts independently. A second regular container is the wrong answer when the task says sidecar (post-2025 curriculum distinction).
Grader: restartPolicy on the init container + heartbeats in logs -c shipper.
Q16 — Released PV rescue (6%)
kubectl get pv keeper-pv -o yaml | grep -A5 claimRef # points at the deleted old-claim
kubectl patch pv keeper-pv --type=json -p='[{"op":"remove","path":"/spec/claimRef"}]'
kubectl get pv keeper-pv # Available
# create new-claim (1Gi RWO class keeper) → binds
echo claimRef > /tmp/exam2/q16.txt
Why: Retain + PVC deletion ⇒ Released, and the stale claimRef (with the dead claim's UID) blocks rebinding forever — by design, so an admin consciously reviews data before reuse. Removing claimRef is that conscious act; data and PV survive.
Trap: deleting/recreating the PV — explicitly forbidden by the task.
Grader: same PV object (creation timestamp unchanged) now Bound to new-claim.
Q17 — accessModes mismatch (4%)
kubectl -n ex2-store describe pvc wide-claim # no PV matches: claim wants RWX, PV offers RWO
# PVC spec is immutable in the relevant fields — recreate:
kubectl -n ex2-store delete pvc wide-claim
# recreate identical but accessModes: [ReadWriteOnce] → binds narrow-pv
echo "PVC requested RWX, PV only offers RWO — accessModes must be satisfiable" > /tmp/exam2/q17.txt
Why: binding requires the PV to offer every mode the claim requests; RWX ⊄ {RWO} ⇒ eternal Pending. AccessModes on a PVC can't be edited in place — delete/recreate is the legitimate path (deleting a Pending claim is safe; nothing bound). Grader: Bound + the one-liner.
Meta
- Fault-ordering was the exam: dead scheduler poisoned four other tasks. On the real thing, a cluster-level symptom noticed in your minute-1 skim gets fixed first regardless of its own weight.
- Direction-of-consent idioms (ReferenceGrant in the target ns; PV's claimRef as the binding brake) — Gateway/storage questions increasingly test who authorizes whom, not YAML recall.
- Every "impossible" state had a one-field fix (claimRef, accessModes, probe port, Corefile directive, one
rin a binary name). Harder papers aren't more typing — they're more reading.