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,369 @@
# 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%)
```bash
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%)
```bash
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%)
```yaml
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%)
```yaml
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%)
```bash
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%)
```bash
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%)
```bash
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%)
```bash
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 `--set`s, 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`:
```yaml
resources: [../../base]
namespace: ex-titan
namePrefix: exam-
images:
- name: nginx
newTag: "1.27"
```
```bash
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%)
```bash
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:
```yaml
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%)
```bash
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%)
```bash
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:
```yaml
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%)
```yaml
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}}
```
```bash
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%)
```bash
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%)
```bash
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%)
```bash
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%)
```bash
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
1. **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.
2. **Generators + patch covered 10 of 17 tasks** with zero hand-written YAML from scratch.
3. **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.