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