Files
kodekloud-engineer/kubernetes/mock exams/cka-exam-pack-2-solutions.md

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

  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.