docs: add Kubernetes CKS study notes
This commit is contained in:
369
kubernetes/mock exams/cka-exam-pack-1-solutions.md
Normal file
369
kubernetes/mock exams/cka-exam-pack-1-solutions.md
Normal 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.
|
||||
Reference in New Issue
Block a user