# CKA Mock Exam Pack v2 — SOLUTIONS > ⚠️ Post-attempt autopsy only. Fastest path → why → trap → grader focus. ## Q1 — dead scheduler (8%) ```bash 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%) ```bash 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 } ``` ```bash 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%) ```bash 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%) ```bash 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%) ```bash 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%) ```bash 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%) ```bash 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%) ```yaml 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: ```bash 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`: ```yaml - op: replace path: /spec/replicas value: 3 ``` `/tmp/exam2/kz/kustomization.yaml`: ```yaml namespace: ex2-batch resources: [deploy.yaml] configMapGenerator: - name: board-cfg literals: [MODE=exam] patches: - path: replicas.yaml target: {kind: Deployment, name: board} ``` ```bash kubectl apply -k /tmp/exam2/kz ``` **Why:** configMapGenerator emits `board-cfg-` 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%) ```yaml 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%) ```yaml 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%) ```bash 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%) ```yaml apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: {name: exam-critical} value: 100000 globalDefault: false ``` Deployment adds: ```yaml 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%) ```bash 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: ```yaml 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%) ```bash 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%) ```bash 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 1. **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. 2. **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. 3. **Every "impossible" state had a one-field fix** (claimRef, accessModes, probe port, Corefile directive, one `r` in a binary name). Harder papers aren't more typing — they're more reading.