docs: add Kubernetes CKS study notes

This commit is contained in:
2026-08-04 23:18:45 +02:00
parent a6ee7a2b07
commit 91a1849009
57 changed files with 8313 additions and 0 deletions

View File

@@ -0,0 +1,315 @@
# 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-<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%)
```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.