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,264 @@
# CKA Mock Exam Pack v1 — full 120-minute simulation
> **Format-matched to the real thing:** 17 weighted tasks, 120 minutes, 66% to pass.
> Domain distribution ≈ real curriculum: Troubleshooting ~30%, Cluster Architecture ~25%, Services & Networking ~20%, Workloads & Scheduling ~15%, Storage ~10%.
> Range: kind cluster `drills` (1 CP + 2 workers, Calico, Gateway API CRDs, metrics-server, helm).
>
> **Kind-isms (read once, then forget):** the real exam says `ssh nodeX` — here it's `docker exec -it drills-worker bash` (or `drills-worker2`, `drills-control-plane`). Real exam switches kubectl contexts per task — here it's one context; the discipline you're simulating is *reading the target of every task carefully*.
---
## EXAM PROTOCOL
1. Run the entire **MASTER SETUP** below. Do not read it — it contains spoilers (it breaks things on purpose). Pipe it to a file and execute blind:
copy the block into `/tmp/setup.sh`, then `bash /tmp/setup.sh > /tmp/setup.log 2>&1`.
2. Set a hard 120:00 timer. No pauses, no notes, no Claude.
3. Solve in any order. Flag anything stuck >8 min and move on. Reserve the last 10 minutes for a verification lap.
4. When the timer dies: run the **VALIDATION** blocks, fill the score sheet, compute your percentage from the weights.
5. Paste score sheet + your command history to Claude for the autopsy.
---
## MASTER SETUP (do not read — paste and run)
```bash
#!/usr/bin/env bash
set +e
# --- namespaces
for ns in ex-neptune ex-mars ex-venus ex-pluto ex-saturn ex-mercury ex-titan ex-io ex-rescue; do kubectl create ns $ns; done
# Q1: kill kubelet on worker2
docker exec drills-worker2 systemctl stop kubelet
# Q5: broken deployment — missing CM + bad image tag
kubectl -n ex-neptune create deploy web-portal --image=nginx:1.99-fake --replicas=2
kubectl -n ex-neptune set env deploy/web-portal --from=configmap/portal-config 2>/dev/null
kubectl -n ex-neptune patch deploy web-portal --type=json -p='[{"op":"add","path":"/spec/template/spec/containers/0/envFrom","value":[{"configMapRef":{"name":"portal-config"}}]}]'
# Q6: service with selector mismatch
kubectl -n ex-mars create deploy api-backend --image=nginx --replicas=2
kubectl -n ex-mars label deploy api-backend app=api-backend --overwrite
kubectl -n ex-mars apply -f - <<'EOF'
apiVersion: v1
kind: Service
metadata: {name: api-svc, namespace: ex-mars}
spec:
selector: {app: api-backent}
ports: [{port: 80, targetPort: 80}]
EOF
# Q16: crashing pod
kubectl -n ex-rescue run data-proc --image=busybox --restart=Never -- sh -c 'echo "FATAL: config /etc/proc/settings.ini not found" >&2; exit 1'
# Q17: broken static pod on worker
docker exec drills-worker bash -c 'mkdir -p /etc/kubernetes/manifests && cat > /etc/kubernetes/manifests/edge-cache.yaml <<EOF
apiVersion: v1
kind: Pod
metadata: {name: edge-cache}
spec:
containers:
- name: cache
image: redis:7-alpinee
ports: [{containerPort: 6379}]
EOF'
# Q4: gateway for httproute task
kubectl apply -f - <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata: {name: exam-class}
spec: {controllerName: example.com/exam}
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata: {name: main-gate, namespace: ex-venus}
spec:
gatewayClassName: exam-class
listeners: [{name: http, port: 80, protocol: HTTP}]
EOF
kubectl -n ex-venus create deploy web-v1 --image=hashicorp/http-echo -- /http-echo -text=v1 -listen=:5678
kubectl -n ex-venus create deploy web-v2 --image=hashicorp/http-echo -- /http-echo -text=v2 -listen=:5678
kubectl -n ex-venus expose deploy web-v1 --port=5678
kubectl -n ex-venus expose deploy web-v2 --port=5678
# Q3: netpol targets
kubectl -n ex-pluto run frontend --image=nginx --labels=role=frontend --port=80
kubectl -n ex-pluto run backend --image=nginx --labels=role=backend --port=80
kubectl -n ex-pluto run cache --image=nginx --labels=role=cache --port=80
# Q7: rbac targets
kubectl -n ex-saturn create sa deploy-bot
# Q11: rollout history to inspect
kubectl -n ex-mercury create deploy release-app --image=nginx:1.25
kubectl -n ex-mercury set image deploy/release-app nginx=nginx:1.26
kubectl -n ex-mercury set image deploy/release-app nginx=nginx:1.27-bogus
# Q12: scheduling
kubectl taint node drills-worker tier=critical:NoSchedule --overwrite
kubectl label node drills-worker zone=east --overwrite
kubectl label node drills-worker2 zone=west --overwrite
# Q13/Q14: storage
kubectl apply -f - <<'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: {name: local-manual}
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
EOF
docker exec drills-worker mkdir -p /opt/vol-alpha
kubectl -n ex-io create deploy metrics-writer --image=busybox -- sleep 3600
echo "SETUP DONE"
```
---
## THE EXAM — 120:00 starts now
### Q1 — 8% — Troubleshooting
Node `drills-worker2` is `NotReady`. Find the cause, fix it, make the node `Ready` again. Write the name of the failed component into `/tmp/exam/q1-component.txt`.
### Q2 — 8% — Cluster Architecture
Create a snapshot of the cluster's etcd database and save it **inside the control-plane node** at `/root/etcd-backup.db`. Then verify the snapshot and write the number of keys/revision info into `/tmp/exam/q2-status.txt` on your host.
*(Hint allowed by the real exam's phrasing: certs live where kubeadm puts them. Enter the CP with `docker exec -it drills-control-plane bash`.)*
### Q3 — 6% — Services & Networking
In `ex-pluto`: Pods `role=backend` must accept ingress **only** from Pods `role=frontend` in the same namespace on port 80. Traffic from `role=cache` (and everything else) must be denied. Name the policy `backend-shield`. Egress stays open.
### Q4 — 7% — Services & Networking
In `ex-venus`, attached to the existing Gateway `main-gate`, create HTTPRoute `release-route`: all requests to path prefix `/` go to `web-v1:5678`, **except** requests with header `X-Version: v2` (exact) which go to `web-v2:5678`. Spec-level correctness counts (no dataplane on this range).
### Q5 — 7% — Troubleshooting
Deployment `web-portal` in `ex-neptune` has 0/2 ready. There are **two independent faults**. Fix both. Desired end state: 2/2 Ready, env sourced from a ConfigMap named `portal-config` containing key `MODE=production`.
### Q6 — 6% — Troubleshooting
Service `api-svc` in `ex-mars` returns no endpoints. Diagnose and repair **the service** (the deployment is correct). Prove endpoints exist afterward.
### Q7 — 7% — Cluster Architecture
In `ex-saturn`, using **imperative commands only**: allow ServiceAccount `deploy-bot` to create, list, and delete Deployments in `ex-saturn` — nothing else, namespace-scoped. Verify with `kubectl auth can-i` (one positive, one negative check) and save both outputs to `/tmp/exam/q7-cani.txt`.
### Q8 — 6% — Cluster Architecture
Using Helm: install chart `bitnami/redis` as release `cache-layer` in namespace `ex-titan`, chart version **one minor behind latest**, with `architecture=standalone` and auth disabled (`auth.enabled=false`) set via CLI. Then upgrade to the latest chart version preserving both overrides. Record `helm history` output to `/tmp/exam/q8-history.txt`.
### Q9 — 4% — Cluster Architecture
A base kustomization exists at `/tmp/drill-kz/base` (from drill pack v1; recreate it if you cleaned it). Create overlay `/tmp/drill-kz/overlays/exam` that deploys into namespace `ex-titan` with name prefix `exam-` and image `nginx:1.27`. Apply it with `-k`.
### Q10 — 5% — Workloads & Scheduling
Create HPA `writer-hpa` in `ex-io` for deployment `metrics-writer`: min 1, max 4, target 70% average CPU. Scale-down stabilization window 240s. (The deployment lacks CPU requests — part of the task is making the HPA functional.)
### Q11 — 5% — Workloads & Scheduling
Deployment `release-app` in `ex-mercury` is stuck mid-rollout on a bad image. Roll it back to the **previous working revision**, then record: current image and the revision number you rolled back to → `/tmp/exam/q11-rollback.txt`.
### Q12 — 5% — Workloads & Scheduling
Create deployment `edge-daemon` (image `busybox`, `sleep 3600`, 2 replicas) in namespace `ex-io` that runs **only on `drills-worker`**: tolerate its taint and pin via nodeAffinity on the `zone=east` label. Both replicas must land there.
### Q13 — 5% — Storage
Create PV `vol-alpha`: 2Gi, RWO, storageClassName `local-manual`, hostPath `/opt/vol-alpha`. Create PVC `claim-alpha` in `ex-io` requesting 1Gi, same class. Explain in one line inside `/tmp/exam/q13-why.txt` why the PVC is Pending at this point. Then mount it in a pod `vol-user` (nginx) at `/data` and confirm Bound.
### Q14 — 5% — Storage
Make `local-manual` the **default** StorageClass of the cluster. Then create PVC `claim-default` in `ex-io` with **no storageClassName** and prove (one command's output to `/tmp/exam/q14-proof.txt`) that it was assigned `local-manual` automatically.
### Q15 — 5% — Services & Networking
In `ex-mars`, create an Ingress `api-ingress` (imperative generator allowed): host `api.exam.local`, path `/v1` (Prefix) → service `api-svc:80`, ingressClassName `nginx`. The controller isn't installed — spec-level correctness counts. Ensure pathType is explicitly `Prefix`.
### Q16 — 6% — Troubleshooting
Pod `data-proc` in `ex-rescue` is failing. Save **its complete logs** (the error line included) to `/tmp/exam/q16-logs.txt`, then recreate the pod so it runs successfully with the same name and image, sleeping 1 hour instead of crashing.
### Q17 — 5% — Troubleshooting
A static pod `edge-cache` was configured on node `drills-worker` but never appears in `kubectl get pods`. Enter the node, find the fault, fix it, and get the mirror pod visible and Running in the `default` namespace.
---
## VALIDATION (run only after the timer — expected outputs in comments)
```bash
EXAM_DIR="${EXAM_DIR:-./exam}"
mkdir -p "$EXAM_DIR"
# Q1
kubectl get node drills-worker2 # Ready
cat "$EXAM_DIR/q1-component.txt" # kubelet
# Q2
docker exec drills-control-plane ls -la /root/etcd-backup.db # exists, >1MB
cat "$EXAM_DIR/q2-status.txt" # snapshot status table/json
# Q3
PIP=$(kubectl -n ex-pluto get pod backend -o jsonpath='{.status.podIP}')
kubectl -n ex-pluto exec frontend -- curl -s -m 2 $PIP | head -1 # HTML (allowed)
kubectl -n ex-pluto exec cache -- curl -s -m 2 $PIP; echo EXIT=$? # EXIT=28 (blocked)
# Q4
kubectl -n ex-venus get httproute release-route -o yaml
# header match in same rule-entry as its path; default rule present; parentRef=main-gate
# Q5
kubectl -n ex-neptune get deploy web-portal # 2/2
kubectl -n ex-neptune get cm portal-config -o jsonpath='{.data.MODE}' # production
# Q6
kubectl -n ex-mars get endpoints api-svc # two pod IPs
# Q7
cat "$EXAM_DIR/q7-cani.txt" # yes + no
kubectl auth can-i delete deployments --as=system:serviceaccount:ex-saturn:deploy-bot -n ex-saturn # yes
kubectl auth can-i create pods --as=system:serviceaccount:ex-saturn:deploy-bot -n ex-saturn # no
# Q8
helm list -n ex-titan # cache-layer, latest chart
helm get values cache-layer -n ex-titan # architecture: standalone, auth.enabled: false
cat "$EXAM_DIR/q8-history.txt" # rev1 + rev2
# Q9
kubectl -n ex-titan get deploy exam-portal -o jsonpath='{.spec.template.spec.containers[0].image}' # nginx:1.27
# Q10
kubectl -n ex-io get hpa writer-hpa -o yaml | grep -E 'averageUtilization|stabilizationWindowSeconds'
# 70 + 240 under scaleDown; deploy now has cpu requests
# Q11
kubectl -n ex-mercury get deploy release-app -o jsonpath='{.spec.template.spec.containers[0].image}' # nginx:1.26
cat "$EXAM_DIR/q11-rollback.txt"
# Q12
kubectl -n ex-io get pods -l app=edge-daemon -o wide # both on drills-worker
# Q13
kubectl -n ex-io get pvc claim-alpha # Bound
cat "$EXAM_DIR/q13-why.txt" # WaitForFirstConsumer explanation
# Q14
kubectl get sc local-manual -o jsonpath='{.metadata.annotations.storageclass\.kubernetes\.io/is-default-class}' # true
cat "$EXAM_DIR/q14-proof.txt" # claim-default → local-manual
# Q15
kubectl -n ex-mars get ingress api-ingress -o jsonpath='{.spec.rules[0].http.paths[0].pathType}' # Prefix
# Q16
grep FATAL "$EXAM_DIR/q16-logs.txt" # the error line
kubectl -n ex-rescue get pod data-proc # Running
# Q17
kubectl get pod edge-cache-drills-worker # Running
```
> **Configurable path:** the validation block reads/writes under `$EXAM_DIR` (defaults to `/tmp/exam`). Override by exporting it before running, e.g. `EXAM_DIR=/tmp/exam-attempt2 bash -c '...'` or `export EXAM_DIR=/tmp/exam-attempt2` beforehand. The exam task descriptions above still reference the literal `/tmp/exam/...` paths — either keep writing there during the exam, or export the same `EXAM_DIR` value before starting the timer and use `$EXAM_DIR/...` for every task's output path too.
## SCORE SHEET
| Q | Weight | Domain | Pass? | Time |
|---|---|---|---|---|
| 1 | 8 | TS | | |
| 2 | 8 | CA | | |
| 3 | 6 | SN | | |
| 4 | 7 | SN | | |
| 5 | 7 | TS | | |
| 6 | 6 | TS | | |
| 7 | 7 | CA | | |
| 8 | 6 | CA | | |
| 9 | 4 | CA | | |
| 10 | 5 | WS | | |
| 11 | 5 | WS | | |
| 12 | 5 | WS | | |
| 13 | 5 | ST | | |
| 14 | 5 | ST | | |
| 15 | 5 | SN | | |
| 16 | 6 | TS | | |
| 17 | 5 | TS | | |
**Score = sum of passed weights. Pass line: 66.**
## RESET (between attempts)
```bash
for ns in ex-neptune ex-mars ex-venus ex-pluto ex-saturn ex-mercury ex-titan ex-io ex-rescue; do kubectl delete ns $ns --wait=false; done
kubectl delete gatewayclass exam-class; kubectl delete pv vol-alpha
kubectl taint node drills-worker tier=critical:NoSchedule- 2>/dev/null
kubectl label node drills-worker zone- ; kubectl label node drills-worker2 zone-
kubectl patch sc local-manual -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
docker exec drills-worker rm -f /etc/kubernetes/manifests/edge-cache.yaml
docker exec drills-worker2 systemctl start kubelet 2>/dev/null
kubectl delete pod edge-cache-drills-worker --force 2>/dev/null
```

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.

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.

View File

@@ -0,0 +1,237 @@
# CKA Mock Exam Pack v2 — 120 min, harder cut
> 17 weighted tasks, 120 minutes, pass = 66. Range: kind `drills` (+ Cilium, Gateway API CRDs, metrics-server, helm).
> **Difficulty delta vs v1:** control-plane components are broken and *tasks interact* — damage you don't fix early will block tasks you attempt later. Triage is part of the score.
> Kind-isms: `ssh nodeX` → `docker exec -it drills-<node> bash`. One kubectl context.
## PROTOCOL
1. Save MASTER SETUP to `/tmp/setup2.sh`, run blind: `bash /tmp/setup2.sh > /tmp/setup2.log 2>&1`. **Do not read it — spoilers.**
2. `mkdir -p /tmp/exam2`. Hard 120:00 timer. No pauses, no Claude, no solutions file.
3. Any order. Flag >8 min. Last 10 min = verification lap.
4. After the timer: run VALIDATE blocks, fill the score sheet, then open the solutions doc for the autopsy.
## MASTER SETUP (paste blind)
```bash
#!/usr/bin/env bash
set +e
for ns in ex2-neptune ex2-web ex2-canary ex2-data ex2-guard ex2-front ex2-batch ex2-side ex2-store; do kubectl create ns $ns; done
# workloads FIRST (scheduler dies later)
kubectl -n ex2-front create deploy shop-ui --image=nginx --replicas=2
kubectl -n ex2-front patch deploy shop-ui --type=json -p='[{"op":"add","path":"/spec/template/spec/containers/0/readinessProbe","value":{"httpGet":{"path":"/","port":8080},"periodSeconds":5}}]'
kubectl -n ex2-data run db --image=nginx --labels=role=db --port=80
kubectl -n ex2-data run app --image=nginx --labels=role=app --port=80
kubectl -n ex2-data run rogue --image=nginx --labels=role=rogue --port=80
kubectl -n ex2-web create deploy web-v1 --image=hashicorp/http-echo -- /http-echo -text=v1 -listen=:5678
kubectl -n ex2-web expose deploy web-v1 --port=5678
kubectl -n ex2-canary create deploy web-v2 --image=hashicorp/http-echo -- /http-echo -text=v2 -listen=:5678
kubectl -n ex2-canary expose deploy web-v2 --port=5678
kubectl apply -f - <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata: {name: exam2-class}
spec: {controllerName: example.com/exam2}
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata: {name: shop-gate, namespace: ex2-web}
spec:
gatewayClassName: exam2-class
listeners: [{name: http, port: 80, protocol: HTTP}]
EOF
kubectl -n ex2-neptune create deploy pinned --image=nginx --replicas=2
kubectl -n ex2-neptune patch deploy pinned --type=json -p='[{"op":"add","path":"/spec/template/spec/nodeSelector","value":{"kubernetes.io/hostname":"drills-worker2"}}]'
kubectl apply -f - <<'EOF'
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: {name: pinned-pdb, namespace: ex2-neptune}
spec:
maxUnavailable: 0
selector: {matchLabels: {app: pinned}}
EOF
kubectl label ns ex2-guard env=guarded
# storage scenarios
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolume
metadata: {name: keeper-pv}
spec:
capacity: {storage: 1Gi}
accessModes: [ReadWriteOnce]
persistentVolumeReclaimPolicy: Retain
storageClassName: keeper
hostPath: {path: /opt/keeper}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: old-claim, namespace: ex2-store}
spec:
accessModes: [ReadWriteOnce]
storageClassName: keeper
resources: {requests: {storage: 1Gi}}
EOF
sleep 5; kubectl -n ex2-store delete pvc old-claim # -> PV Released
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolume
metadata: {name: narrow-pv}
spec:
capacity: {storage: 1Gi}
accessModes: [ReadWriteOnce]
storageClassName: narrow
hostPath: {path: /opt/narrow}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: wide-claim, namespace: ex2-store}
spec:
accessModes: [ReadWriteMany]
storageClassName: narrow
resources: {requests: {storage: 1Gi}}
EOF
# broken helm chart
mkdir -p /tmp/exam2 && cd /tmp/exam2 && helm create shipper >/dev/null 2>&1
sed -i 's/^apiVersion: v2/apiVersion: v3/' /tmp/exam2/shipper/Chart.yaml
sed -i 's/{{ .Values.replicaCount }}/{{ .Values.replicaCount }/' /tmp/exam2/shipper/templates/deployment.yaml
# broken kubeconfig for developer
kubectl config view --raw --minify > /tmp/exam2/developer.kubeconfig
SRV=$(grep server /tmp/exam2/developer.kubeconfig | awk '{print $2}')
PORT=${SRV##*:}; NEW=$((PORT+1))
sed -i "s|$SRV|${SRV%:*}:$NEW|" /tmp/exam2/developer.kubeconfig
kubectl -n ex2-front wait --for=condition=Available deploy --all --timeout=90s
kubectl -n ex2-data wait --for=condition=Ready pod --all --timeout=90s
# BREAK coredns
kubectl -n kube-system get cm coredns -o yaml > /tmp/exam2/.coredns-backup.yaml
kubectl -n kube-system patch cm coredns --type merge -p '{"data":{"Corefile":".:53 {\n errors\n forward . /etc/resolv.conf\n bogusplugin\n}\n"}}'
kubectl -n kube-system rollout restart deploy coredns
# BREAK scheduler (last)
docker exec drills-control-plane sed -i 's|kube-scheduler|kube-schedulerr|' /etc/kubernetes/manifests/kube-scheduler.yaml
echo "SETUP2 DONE"
```
---
## THE EXAM
### Q1 — 8% — Troubleshooting
New Pods across the cluster stay `Pending` with no scheduling events. A control-plane component is at fault. Find it, fix it on the node, prove new pods schedule again. Component name → `/tmp/exam2/q1.txt`.
### Q2 — 7% — Troubleshooting
Cluster DNS is down (CoreDNS crash-looping). Diagnose and repair so that a busybox:1.28 pod can `nslookup kubernetes.default`. Root cause, one line → `/tmp/exam2/q2.txt`.
### Q3 — 6% — Troubleshooting
The kubeconfig at `/tmp/exam2/developer.kubeconfig` is broken — `kubectl --kubeconfig /tmp/exam2/developer.kubeconfig get nodes` fails. Fix **the file** (not your admin config) so the command succeeds. Fault, one line → `/tmp/exam2/q3.txt`.
### Q4 — 5% — Troubleshooting
Drain node `drills-worker2` (ignore DaemonSets). Something will block it. Resolve the blocker with the **minimal** change that still keeps ≥1 replica of the affected app available at all times, complete the drain, then uncordon. Blocker → `/tmp/exam2/q4.txt`.
### Q5 — 4% — Troubleshooting
Deployment `shop-ui` in `ex2-front` shows 0/2 READY though containers run. Find why and fix the deployment (the container itself is fine).
### Q6 — 8% — Cluster Architecture
Take an etcd snapshot to `/root/etcd-v2.db` on the control-plane node. Then create ConfigMap `marker` (`kubectl create cm marker -n default --from-literal=state=after-backup`), and **restore the snapshot** so that `marker` no longer exists. Prove it.
### Q7 — 6% — Cluster Architecture
The Helm chart at `/tmp/exam2/shipper` fails to install. Find and fix **all** faults (start with `helm lint`), then install it as release `shipper` in namespace `ex2-batch` with 2 replicas set via CLI.
### Q8 — 6% — Cluster Architecture
Namespace `ex2-guard` is labeled `env=guarded`. Create a ValidatingAdmissionPolicy `require-owner` + binding: every Deployment created in namespaces labeled `env=guarded` must carry a label `owner` (any value); violations are **denied** with a message mentioning "owner". Prove: one denied create, one accepted.
### Q9 — 5% — Cluster Architecture
Under `/tmp/exam2/kz`, build a kustomization (no base reuse — from scratch): deploys `nginx:1.27` deployment `board`, namespace `ex2-batch`, replicas patched to 3 via a **JSON6902 patch**, plus a generated ConfigMap `board-cfg` with `MODE=exam`. Apply with `-k`. The deployment must reference the generated ConfigMap via `envFrom` (hash suffix handled by kustomize).
### Q10 — 8% — Services & Networking
Create HTTPRoute `split-route` in `ex2-web` on Gateway `shop-gate`: path prefix `/shop`, traffic split **90%** to `web-v1:5678` (same ns) and **10%** to `web-v2:5678` — which lives in namespace `ex2-canary`. Make the cross-namespace backend reference legal. Spec-level correctness counts.
### Q11 — 7% — Services & Networking
In `ex2-data`: Pods `role=app` may send egress **only** to Pods `role=db` on TCP 80, plus DNS (TCP+UDP 53) anywhere. Everything else outbound denied. Policy name `app-egress`. Prove: app→db works, app→rogue times out.
### Q12 — 5% — Services & Networking
Expose deployment `web-v1` in `ex2-web` via a NodePort service `web-np` on port 5678, nodePort **30080**. Prove reachability with curl **from a node** (docker exec).
### Q13 — 6% — Workloads & Scheduling
Create PriorityClass `exam-critical` (value 100000, not default). In `ex2-batch` create deployment `spread-app` (nginx, 4 replicas, priorityClassName `exam-critical`) with a topologySpreadConstraint: maxSkew 1 over `kubernetes.io/hostname`, `DoNotSchedule`. End state: 2 pods per worker.
### Q14 — 5% — Workloads & Scheduling
Create DaemonSet `node-agent` in `ex2-batch` (busybox, `sleep 3600`) that runs on **all three nodes including the control-plane**. Prove 3/3.
### Q15 — 4% — Workloads & Scheduling
In `ex2-side`: deployment `audit-app` (1 replica): main container `app` (busybox) writes the date to `/var/log/audit/audit.log` every 5s; **native sidecar** `shipper` (busybox) tails it. Shared emptyDir. Prove heartbeats via `kubectl logs ... -c shipper`.
### Q16 — 6% — Storage
PV `keeper-pv` is `Released` and must be reused **without deleting the PV or its data**. Make it bindable again and bind a new PVC `new-claim` (1Gi, RWO, class `keeper`) in `ex2-store`. Both must reach `Bound`. What field did you touch → `/tmp/exam2/q16.txt`.
### Q17 — 4% — Storage
PVC `wide-claim` in `ex2-store` is `Pending` and will never bind. Diagnose; fix by changing **the claim** so it binds to PV `narrow-pv`. Root cause → `/tmp/exam2/q17.txt`.
---
## VALIDATE (after the timer)
```bash
# Q1
kubectl -n default run probe --image=nginx --restart=Never && sleep 5 && kubectl get pod probe # Running
cat /tmp/exam2/q1.txt # kube-scheduler
# Q2
kubectl run dnstest --image=busybox:1.28 --restart=Never --rm -it -- nslookup kubernetes.default # resolves
# Q3
kubectl --kubeconfig /tmp/exam2/developer.kubeconfig get nodes # 3 nodes
# Q4
kubectl get node drills-worker2 # Ready, SchedulingDisabled absent
kubectl -n ex2-neptune get pdb pinned-pdb -o jsonpath='{.spec}' # loosened but still protective
kubectl -n ex2-neptune get deploy pinned # 2/2 (back after uncordon or rescheduled)
# Q5
kubectl -n ex2-front get deploy shop-ui # 2/2
# Q6
kubectl get cm marker -n default # NotFound (proof of restore)
docker exec drills-control-plane ls -la /root/etcd-v2.db # exists
# Q7
helm list -n ex2-batch | grep shipper # deployed
kubectl -n ex2-batch get deploy -l app.kubernetes.io/name=shipper -o jsonpath='{.items[0].spec.replicas}' # 2
# Q8
kubectl -n ex2-guard create deploy bad --image=nginx 2>&1 | grep -i owner # denied, message mentions owner
kubectl -n ex2-guard create deploy good --image=nginx && kubectl -n ex2-guard label deploy good owner=me # wrong order — see solutions
kubectl -n ex2-guard delete deploy good 2>/dev/null
# Q9
kubectl -n ex2-batch get deploy board -o jsonpath='{.spec.replicas}' # 3
kubectl -n ex2-batch get cm | grep board-cfg # board-cfg-<hash>
# Q10
kubectl -n ex2-web get httproute split-route -o yaml # weights 90/10, ns on the v2 backendRef
kubectl -n ex2-canary get referencegrant -o yaml # allows HTTPRoute/ex2-web -> Service
# Q11
DBIP=$(kubectl -n ex2-data get pod db -o jsonpath='{.status.podIP}')
RGIP=$(kubectl -n ex2-data get pod rogue -o jsonpath='{.status.podIP}')
kubectl -n ex2-data exec app -- curl -s -m 2 $DBIP >/dev/null && echo DB-OK
kubectl -n ex2-data exec app -- curl -s -m 2 $RGIP; echo EXIT=$? # EXIT=28
# Q12
docker exec drills-worker curl -s -m 2 localhost:30080 # v1
# Q13
kubectl -n ex2-batch get pods -l app=spread-app -o wide # 2 + 2 across workers
# Q14
kubectl -n ex2-batch get ds node-agent # DESIRED 3, READY 3
# Q15
kubectl -n ex2-side logs deploy/audit-app -c shipper | tail -3 # dated lines
kubectl -n ex2-side get pod -o jsonpath='{.items[0].spec.initContainers[0].restartPolicy}' # Always
# Q16
kubectl get pv keeper-pv # Bound to ex2-store/new-claim
cat /tmp/exam2/q16.txt # claimRef
# Q17
kubectl -n ex2-store get pvc wide-claim # Bound
cat /tmp/exam2/q17.txt # accessModes mismatch
```
## SCORE SHEET
| Q | W | Dom | Pass | Time | | Q | W | Dom | Pass | Time |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 8 | TS | | | | 10 | 8 | SN | | |
| 2 | 7 | TS | | | | 11 | 7 | SN | | |
| 3 | 6 | TS | | | | 12 | 5 | SN | | |
| 4 | 5 | TS | | | | 13 | 6 | WS | | |
| 5 | 4 | TS | | | | 14 | 5 | WS | | |
| 6 | 8 | CA | | | | 15 | 4 | WS | | |
| 7 | 6 | CA | | | | 16 | 6 | ST | | |
| 8 | 6 | CA | | | | 17 | 4 | ST | | |
| 9 | 5 | CA | | | | | | | | |
**Pass: 66.** Rebuild the range between attempts (`kind delete cluster --name drills` + range-up) — v2's damage is deep enough that RESET scripts lie.

View File

@@ -0,0 +1,130 @@
# CKA Range Bring-Up — kind + Cilium (matches Mock Exam Pack v1)
> Produces cluster `drills` on Kubernetes **v1.35.0** (= exam version) with nodes named exactly as the drill/mock packs expect: `drills-control-plane`, `drills-worker`, `drills-worker2`.
> CNI: **Cilium 1.19.x** — real NetworkPolicy enforcement (kindnet has none; netpol drills would silently pass on it).
> Versions verified 2026-07-29. Re-check pins if you're reading this much later: kind releases page, cilium.io stable docs.
---
## 0. Host prerequisites
- Docker (or compatible), ≥ 8GB RAM free for the 3 nodes
- **cgroup v2 on the host** — mandatory: k8s 1.35 node images dropped cgroup v1. Check: `stat -fc %T /sys/fs/cgroup` → must print `cgroup2fs`. Any current distro qualifies.
- Tools: `kind` ≥ v0.31.0 (ships the v1.35.0 default image), `kubectl`, `helm`
```bash
kind version # v0.31.x
helm version # v3.x
```
## 1. Cluster config — `kind-drills.yaml`
```yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: drills # ⇒ node names drills-control-plane / drills-worker / drills-worker2
networking:
disableDefaultCNI: true # kindnet out, Cilium in
nodes:
- role: control-plane
image: kindest/node:v1.35.0 # pin explicitly = exam version, survives kind upgrades
- role: worker
image: kindest/node:v1.35.0
- role: worker
image: kindest/node:v1.35.0
```
Note: kube-proxy stays (default). Do NOT enable Cilium's kube-proxy replacement — the exam cluster runs kube-proxy, and iptables-visible Services are part of the troubleshooting surface you're training.
## 2. Bring-up — `range-up.sh`
```bash
#!/usr/bin/env bash
set -euo pipefail
CILIUM_VER=1.19.6
kind create cluster --config kind-drills.yaml
# nodes will sit NotReady until CNI lands — expected
# --- Cilium (per cilium.io kind guide) ---
docker pull quay.io/cilium/cilium:v${CILIUM_VER}
kind load docker-image quay.io/cilium/cilium:v${CILIUM_VER} --name drills # skip registry pulls on each node
helm repo add cilium https://helm.cilium.io/ 2>/dev/null; helm repo update
helm install cilium cilium/cilium --version ${CILIUM_VER} \
--namespace kube-system \
--set image.pullPolicy=IfNotPresent \
--set ipam.mode=kubernetes
kubectl -n kube-system rollout status ds/cilium --timeout=180s
kubectl wait --for=condition=Ready node --all --timeout=180s
# --- Gateway API CRDs (standard channel) ---
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml
# --- metrics-server (for HPA TARGETS + kubectl top) ---
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
kubectl -n kube-system patch deploy metrics-server --type=json \
-p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'
# ^ mandatory on kind: kubelet serves self-signed certs; without this metrics-server never goes Ready
# --- helm repo used by drill K4 / mock Q8 ---
helm repo add bitnami https://charts.bitnami.com/bitnami 2>/dev/null; helm repo update
echo "RANGE UP"
```
## 3. Smoke test — run before ANY drill session
```bash
kubectl get nodes -o wide
# drills-control-plane / drills-worker / drills-worker2 — all Ready, VERSION v1.35.0
kubectl get pods -A | grep -v Running | grep -v Completed # empty
kubectl api-resources | grep -i httproute # gateway.networking.k8s.io present
kubectl top nodes # numbers (may need ~60s after install)
kubectl -n kube-system exec ds/cilium -- cilium status --brief # OK
# NetworkPolicy ENFORCEMENT check — the one that matters (kindnet would pass traffic anyway):
kubectl create ns smoke
kubectl -n smoke run a --image=nginx --labels=app=a --port=80
kubectl -n smoke run b --image=busybox -- sleep 600
kubectl -n smoke wait --for=condition=Ready pod --all --timeout=60s
kubectl -n smoke apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: deny-all}
spec:
podSelector: {}
policyTypes: [Ingress]
EOF
AIP=$(kubectl -n smoke get pod a -o jsonpath='{.status.podIP}')
kubectl -n smoke exec b -- wget -qO- -T 2 $AIP && echo "FAIL: netpol NOT enforced" || echo "OK: netpol enforced"
kubectl delete ns smoke --wait=false
```
If that last line prints FAIL, the CNI install went sideways — do not run K1/Q3 until fixed (`kubectl -n kube-system logs ds/cilium | tail`).
## 4. Static pod path sanity (mock Q17 depends on it)
```bash
docker exec drills-worker grep staticPodPath /var/lib/kubelet/config.yaml
# expect: staticPodPath: /etc/kubernetes/manifests
```
## 5. Teardown / rebuild
```bash
kind delete cluster --name drills # full nuke, ~10s
# rebuild = run range-up.sh again, ~3 min total
```
Rebuild is cheap — prefer a fresh cluster over archaeologically cleaning a broken one between mock attempts. The mock pack's RESET script is for same-day reruns; a new day gets a new cluster.
## 6. Known deltas vs the real exam (accept, don't fight)
| Real exam | This range |
|---|---|
| `ssh nodeX` | `docker exec -it drills-<node> bash` |
| multiple kubectl contexts | one context; discipline = read each task's target ns/node |
| kubeadm package upgrades (apt) | not possible — node image is baked; upgrade tasks stay on KodeKloud/killer |
| several CNIs possible | Cilium everywhere (netpol semantics are standard, so drills transfer) |

View File

@@ -0,0 +1,421 @@
# CKA Drill Pack v1 — killer-style, self-validating
> Range: kind cluster `drills` (1 CP + 2 workers, Calico, Gateway API CRDs, metrics-server, helm).
> Protocol per drill: paste SETUP blindly → start clock → solve TASK → run VALIDATE → paste commands + time + validation output to Claude for grading.
> Rules: imperative first where a generator exists. Verify before declaring done. Budget is a ceiling, not a goal.
> Cleanup after each drill: `k delete ns <drill-ns> --wait=false` unless noted.
---
## K1 — NetworkPolicy: same-label, wrong-namespace trap — 8 min
**SETUP**
```bash
k create ns drill-np
k -n drill-np run db --image=nginx --labels=tier=db --port=80
k -n drill-np run api --image=nginx --labels=tier=api --port=80
k -n drill-np run web --image=nginx --labels=tier=web --port=80
k create ns drill-ext
k -n drill-ext run outsider --image=busybox --labels=tier=api -- sleep 3600
k -n drill-np wait --for=condition=Ready pod --all --timeout=60s
```
**TASK**
In Namespace `drill-np` create a NetworkPolicy `db-guard`: incoming traffic to Pods labeled `tier: db` allowed **only** on port 80 and **only** from Pods labeled `tier: api` **in the same Namespace**. All other ingress to db Pods blocked. Egress from db Pods unrestricted.
**Solution**
```bash
cat 1-netpol.yaml
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-guard
namespace: drill-np
spec:
podSelector:
matchLabels:
tier: db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
tier: api
ports:
- protocol: TCP
port: 80
```
**VALIDATE**
```bash
DB_IP=$(k -n drill-np get pod db -o jsonpath='{.status.podIP}')
k -n drill-np exec api -- curl -s -m 2 $DB_IP # nginx HTML (allowed)
k -n drill-np exec web -- curl -s -m 2 $DB_IP # timeout / exit 28 (blocked)
k -n drill-ext exec outsider -- wget -qO- -T 2 $DB_IP # timeout (blocked — same label, wrong ns)
```
Third check is the point: if it succeeds, your `from` clause selects by pod label without pinning the namespace.
---
## K2 — HPA: scale-up rate limiting — 7 min
**SETUP**
```bash
k create ns drill-hpa
k -n drill-hpa create deploy checkout --image=nginx --replicas=2
k -n drill-hpa set resources deploy checkout --requests=cpu=100m
```
**TASK**
Create HPA `checkout-hpa` for Deployment `checkout` in `drill-hpa`: min 2, max 6, target average CPU utilization 65%. Scale-**up** limited to at most 1 Pod per 120 seconds. (Up, not down — read your policy block twice.)
**Solution**
```bash
cat 2-hpa.yaml | k neat
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: checkout-hpa
namespace: drill-hpa
spec:
maxReplicas: 6
metrics:
- resource:
name: cpu
target:
averageUtilization: 60
type: Utilization
type: Resource
minReplicas: 2
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: checkout
```
**VALIDATE**
```bash
k -n drill-hpa get hpa checkout-hpa -o yaml | grep -A8 behavior
# scaleUp → policies: [{type: Pods, value: 1, periodSeconds: 120}]
k -n drill-hpa get hpa checkout-hpa -o jsonpath='{.spec.metrics[0].resource.target.averageUtilization}' # 65
k -n drill-hpa get hpa checkout-hpa # TARGETS: cpu%/65% (or <unknown> without metrics-server)
```
---
## K3 — HTTPRoute: header AND/OR semantics — 10 min
**SETUP**
```bash
# cluster prerequisities - GatewayApi
GWAPI=v1.3.0 # match to what your Cilium minor supports; check the Cilium docs for your version
# Standard channel: GatewayClass, Gateway, HTTPRoute, GRPCRoute, ReferenceGrant
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/${GWAPI}/standard-install.yaml
# Cilium additionally expects TLSRoute, which lives only in the experimental channel
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/${GWAPI}/config/crd/experimental/gateway.networking.k8s.io_tlsroutes.yaml
kubectl get crd | grep gateway.networking.k8s.io
# Step 2 — Enable Gateway API in Cilium
helm repo update cilium
helm upgrade cilium cilium/cilium -n kube-system \
--reuse-values \
--set gatewayAPI.enabled=true
# confirm the flag reached the ConfigMap
kubectl -n kube-system get cm cilium-config -o jsonpath='{.data.enable-gateway-api}{"\n"}' # true
## Step 3 — Restart operator and agents (REQUIRED — see Context)
kubectl -n kube-system rollout restart deploy/cilium-operator
kubectl -n kube-system rollout restart ds/cilium
kubectl -n kube-system rollout status deploy/cilium-operator --timeout=180s
kubectl -n kube-system rollout status ds/cilium --timeout=300s
kubectl create ns drill-gw
kubectl -n drill-gw create deploy legacy --image=hashicorp/http-echo -- /http-echo -text=legacy -listen=:8080
kubectl -n drill-gw create deploy beta --image=hashicorp/http-echo -- /http-echo -text=beta -listen=:8080
kubectl -n drill-gw expose deploy legacy --port=8080
kubectl -n drill-gw expose deploy beta --port=8080
kubectl -n drill-gw rollout status deploy/legacy deploy/beta
kubectl apply -f - <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata: {name: drill-gate, namespace: drill-gw}
spec:
gatewayClassName: cilium
listeners:
- {name: http, port: 80, protocol: HTTP, allowedRoutes: {namespaces: {from: Same}}}
EOF
kubectl -n drill-gw wait --for=condition=Programmed gateway/drill-gate --timeout=180s
kubectl -n drill-gw get gateway drill-gate
```
**TASK**
Create HTTPRoute `canary-route` in `drill-gw` attached to Gateway `drill-gate`:
1. `/app` (prefix) with header `X-Canary` exactly `true` → Service `beta:8080`
2. all other `/app``legacy:8080`
3. `/app` with `X-Canary: true` AND query param `debug=1` must ALSO reach `beta` — third rule or covered already? Justify in one sentence.
**VALIDATE** (spec-level — dummy class, no dataplane)
```bash
k -n drill-gw get httproute canary-route -o yaml
# 1: header+path in ONE match entry (AND), not two entries (OR)
# 2: header rule ordered/precedent over bare /app
k -n drill-gw get httproute canary-route -o jsonpath='{.spec.parentRefs[0].name}' # drill-gate
```
---
## K4 — Helm: pin, override, upgrade without losing values — 6 min
**SETUP**
```bash
helm repo add bitnami https://charts.bitnami.com/bitnami 2>/dev/null; helm repo update
```
**TASK**
Install `bitnami/nginx` as release `web-drill` in new namespace `drill-helm`, **exactly 2 replicas** set via CLI (no values file), chart version pinned one minor **behind** latest. Print the user-supplied values of the deployed release. Upgrade to latest chart version **keeping** the replica override.
**VALIDATE**
```bash
helm list -n drill-helm # pinned version, then latest after upgrade
helm get values web-drill -n drill-helm # replicaCount: 2 — must survive the upgrade
k -n drill-helm get deploy # READY 2/2
helm history web-drill -n drill-helm # rev1 install, rev2 upgrade
```
Trap: "keeping your override" — there is a right flag and a wrong assumption.
---
## K5 — Kustomize: overlay surgery (extended) — 14 min
**SETUP**
```bash
mkdir -p /tmp/drill-kz/base /tmp/drill-kz/overlays/prod
cat > /tmp/drill-kz/base/deploy.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: portal
labels: {app: portal}
spec:
replicas: 1
selector: {matchLabels: {app: portal}}
template:
metadata: {labels: {app: portal}}
spec:
containers:
- name: portal
image: nginx:1.25
envFrom:
- configMapRef: {name: portal-config}
EOF
cat > /tmp/drill-kz/base/svc.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
name: portal
labels: {app: portal}
spec:
selector: {app: portal}
ports:
- {port: 80, targetPort: 80}
EOF
cat > /tmp/drill-kz/base/kustomization.yaml <<'EOF'
resources: [deploy.yaml, svc.yaml]
configMapGenerator:
- name: portal-config
literals: [APP_MODE=basic]
EOF
```
**TASK**
Build overlay `prod` at `/tmp/drill-kz/overlays/prod` that, without touching base, does ALL of the following:
1. Sets namespace `drill-prod` on everything.
2. Adds name prefix `prod-` to every resource.
3. Adds common label `env: prod` — must land on the Deployment's metadata, its pod template, **and** its selector/Service selector (labels + selectors stay consistent).
4. Bumps the image to `nginx:1.27` (use the `images` transformer, not a patch).
5. Scales replicas to 3 (use the `replicas` transformer, not a patch).
6. **Extends** the `portal-config` ConfigMap with `LOG_LEVEL=debug` while **keeping** `APP_MODE=basic` — this must be a generator merge, not a redefinition.
7. Adds a **new** `portal-secret` Secret generator (literal `API_KEY=s3cr3t`) and wires it into the container's `envFrom` via a **JSON 6902** patch — reference the secret by its plain generator name and let kustomize's name-reference fixup resolve the real (hashed, prefixed) name for you.
8. Adds container resource limits `cpu: 200m`, `memory: 128Mi` via the same or a second JSON 6902 patch (`resources` isn't set in base — the op must `add`, not `replace`).
Create namespace `drill-prod`, apply the overlay with `-k`.
**VALIDATE**
```bash
kubectl kustomize /tmp/drill-kz/overlays/prod # eyeball everything before applying — 4 objects: Deployment, Service, ConfigMap, Secret
k -n drill-prod get deploy prod-portal -o jsonpath='{.spec.replicas} {.spec.template.spec.containers[0].image}'
# 3 nginx:1.27
k -n drill-prod get deploy prod-portal -o jsonpath='{.metadata.labels.env} {.spec.selector.matchLabels.env} {.spec.template.metadata.labels.env}'
# prod prod prod (object label + selector + template label all consistent)
CM=$(k -n drill-prod get cm -o name | grep prod-portal-config)
k -n drill-prod get $CM -o jsonpath='{.data}'
# {"APP_MODE":"basic","LOG_LEVEL":"debug"} — both keys present, merge not replace
k -n drill-prod get secret -o name | grep prod-portal-secret # generated, hashed, prefixed
k -n drill-prod get deploy prod-portal -o jsonpath='{.spec.template.spec.containers[0].envFrom}'
# two entries — configMapRef and secretRef both pointing at the REAL hashed+prefixed names
# if secretRef.name literally reads "portal-secret", your patch bypassed kustomize's name-reference transformer
k -n drill-prod get deploy prod-portal -o jsonpath='{.spec.template.spec.containers[0].resources.limits}'
# {"cpu":"200m","memory":"128Mi"}
```
Trap: item 7 is the point of the drill — patch against the *pre-transform* name and trust kustomize to rewrite it everywhere; hand-typing the hashed name works today and silently breaks the next time a literal changes.
---
## K6 — CRD + CR: author the schema — 10 min
**SETUP** — none. Blank slate is the drill.
**TASK**
Create CRD `backups.data.example.com`: namespaced, kind `Backup`, plural `backups`, shortName `bk`, version `v1` (served+storage). Schema: `spec.source` (string, required), `spec.retentionDays` (integer, 130). Then create a `Backup` named `nightly` in namespace `drill-crd` with source `pg-main`, retentionDays 7. Then prove validation works: attempt retentionDays 99 and capture the rejection.
**VALIDATE**
```bash
k api-resources | grep backups # data.example.com, bk, Backup
k explain backup.spec # both fields, with types
k -n drill-crd get bk nightly -o jsonpath='{.spec.retentionDays}' # 7
# the 99 attempt must fail with a schema validation error — paste the error line
```
---
## K7 — RBAC + SA + API curl: the 403 chain — 10 min
**SETUP**
```bash
k create ns drill-rbac
k -n drill-rbac create sa auditor
k -n drill-rbac run client --image=nginx:1-alpine --overrides='{"spec":{"serviceAccountName":"auditor"}}'
```
**TASK**
1. Grant SA `auditor` permission to **get and list** ConfigMaps in `drill-rbac` only — imperative commands only, no YAML files.
2. Prove with `kubectl auth can-i` (positive AND negative: it must NOT list secrets).
3. Exec into `client` and curl the ConfigMaps list endpoint with the mounted token — capture the HTTP response kind.
4. Curl the Secrets endpoint the same way — capture the 403.
**VALIDATE**
```bash
k auth can-i list configmaps --as=system:serviceaccount:drill-rbac:auditor -n drill-rbac # yes
k auth can-i list secrets --as=system:serviceaccount:drill-rbac:auditor -n drill-rbac # no
# curl 1 → "kind":"ConfigMapList"
# curl 2 → "kind":"Status","code":403
```
---
## K8 — Scheduling: taints, tolerations, spread — 9 min
**SETUP**
```bash
k taint node dev-cilium-worker dedicated=batch:NoSchedule
k label node dev-cilium-worker pool=batch
k label node dev-cilium-worker2 pool=web
```
**TASK**
1. Create deployment `batch-runner` (image `busybox`, command `sleep 3600`, 2 replicas) in new ns `drill-sched` that runs **only** on `drills-worker` — it must tolerate the taint AND be constrained to that node via its label (both mechanisms, and know why one alone is insufficient).
2. Create pod `web-pin` (nginx) that must land on `drills-worker2` using nodeAffinity (no nodeName).
**VALIDATE**
```bash
k -n drill-sched get pods -o wide # both batch-runner pods on drills-worker, web-pin on drills-worker2
k -n drill-sched get deploy batch-runner -o yaml | grep -A6 tolerations # dedicated=batch:NoSchedule tolerated
k -n drill-sched get pod web-pin -o yaml | grep -A10 affinity # nodeAffinity on pool=web
```
Question to answer in your paste: what happens with toleration but no selector? (One sentence.)
**CLEANUP** (mandatory — taints leak into later drills)
```bash
k taint node drills-worker dedicated=batch:NoSchedule-
k label node drills-worker pool- ; k label node drills-worker2 pool-
```
---
## K9 — Storage: PV/PVC binding chain — 8 min
**SETUP**
```bash
k apply -f - <<'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: {name: drill-manual}
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
EOF
```
**TASK**
1. Create PV `pv-drill`: 1Gi, `ReadWriteOnce`, storageClassName `drill-manual`, hostPath `/tmp/pv-drill`.
2. Create PVC `pvc-drill` in new ns `drill-store`: request 500Mi, same class, RWO.
3. Explain (one sentence) why the PVC stays `Pending` right now — this is not an error.
4. Create pod `store-user` (nginx) mounting the PVC at `/data`; confirm the bind flips.
**VALIDATE**
```bash
k -n drill-store get pvc pvc-drill # Pending BEFORE pod, Bound AFTER
k get pv pv-drill # Bound, CLAIM drill-store/pvc-drill
k -n drill-store exec store-user -- sh -c 'echo ok > /data/probe && cat /data/probe' # ok
```
---
## K10 — Native sidecar + output formatting — 8 min
**SETUP** — none.
**TASK**
1. In new ns `drill-side`, create deployment `tracked-app` (1 replica): main container `app` (busybox) appends the date to `/var/log/app/beat.log` every 5s; **native sidecar** `tail-agent` (busybox) — init container with `restartPolicy: Always` — tails that file. Shared emptyDir at `/var/log/app`.
2. One command: print for every pod in `drill-side` the columns `NAME`, `QOS`, `NODE`, sorted by name, written to `/tmp/drill-side.txt`.
**VALIDATE**
```bash
k -n drill-side get pod -o jsonpath='{.items[0].spec.initContainers[0].restartPolicy}' # Always
k -n drill-side logs deploy/tracked-app -c tail-agent | tail -3 # dated heartbeats flowing
cat /tmp/drill-side.txt # header row + one line: name, QoS class, node
```
---
## Grading rubric (what Claude checks on every paste-back)
1. Correct end state (validation output matches expected)
2. Path efficiency — generator/patch/docs-copy vs hand-written YAML
3. Verification discipline — did you run VALIDATE before declaring done
4. Time vs budget
5. Trap detection — each drill has one; naming it explicitly earns full marks
## Score log
| Drill | Date | Time | Clean? | Trap caught? | Notes |
|---|---|---|---|---|---|
| K1 | | | | | |
| K2 | | | | | |
| K3 | | | | | |
| K4 | | | | | |
| K5 | | | | | |
| K6 | | | | | |
| K7 | | | | | |
| K8 | | | | | |
| K9 | | | | | |
| K10 | | | | | |

View File

@@ -0,0 +1,231 @@
# Task 1
```bash
apiVersion: v1
kind: Pod
metadata:
labels:
run: mc-pod
name: mc-pod
namespace: mc-namespace
spec:
containers:
- image: nginx:1-alpine
name: mc-pod-1
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- image: busybox:1
name: mc-pod-2
command: ["sh", "-c"]
args: ["while true; do date >> /var/log/shared/date.log; sleep 1; done"]
volumeMounts:
- mountPath: /var/log/shared
name: shared-log
- image: busybox:1
name: mc-pod-3
command: ["sh", "-c"]
args: ["tail -f /var/log/shared/date.log"]
volumeMounts:
- mountPath: /var/log/shared
name: shared-log
volumes:
- name: shared-log
emptyDir:
sizeLimit: 500Mi
echo "verticalpodautoscalercheckpoints" >> /root/vpa-crds.txt
echo "verticalpodautoscalers" >> /root/vpa-crds.txt
kubectl expose pod messaging --port=6379 --name=messaging-service
kubectl create deployment hr-web-app --image=kodekloud/webapp-color --replicas=2
cat webapp-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: webapp-hpa
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: kkapp-deploy
minReplicas: 2
maxReplicas: 10
cat pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-analytics
spec:
hostPath:
path: /pv/data-analytics
accessModes:
- ReadWriteMany
capacity:
storage: 100Mi
cat orange.yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: "2026-07-21T19:20:48Z"
generation: 1
name: orange
namespace: default
resourceVersion: "4629"
uid: 3a8c1925-7db8-4518-9489-e6ae2e814ac5
spec:
containers:
- command:
- sh
- -c
- echo The app is running! && sleep 3600
image: busybox:1.28
imagePullPolicy: IfNotPresent
name: orange-container
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: kube-api-access-jhc2d
readOnly: true
dnsPolicy: ClusterFirst
enableServiceLinks: true
initContainers:
- command:
- sh
- -c
- sleep 2;
image: busybox
imagePullPolicy: Always
name: init-myservice
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: kube-api-access-jhc2d
readOnly: true
nodeName: controlplane
preemptionPolicy: PreemptLowerPriority
priority: 0
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
serviceAccount: default
serviceAccountName: default
terminationGracePeriodSeconds: 30
tolerations:
- effect: NoExecute
key: node.kubernetes.io/not-ready
operator: Exists
tolerationSeconds: 300
- effect: NoExecute
key: node.kubernetes.io/unreachable
operator: Exists
tolerationSeconds: 300
volumes:
- name: kube-api-access-jhc2d
projected:
defaultMode: 420
sources:
- serviceAccountToken:
expirationSeconds: 3607
path: token
- configMap:
items:
- key: ca.crt
path: ca.crt
name: kube-root-ca.crt
- downwardAPI:
items:
- fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
path: namespace
kubectl expose deployment hr-web-app --port=8080 --type=NodePort --name=hr-web-app-service
-----------
cat /root/webapp-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: webapp-hpa
spec:
maxReplicas: 10
metrics:
- resource:
name: cpu
target:
averageUtilization: 50
type: Utilization
type: Resource
minReplicas: 1
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: kkapp-deploy
status:
currentMetrics: null
desiredReplicas: 0
----------------------------
# vpa
# ----------------
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: analytics-vpa
namespace: default
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: analytics-deployment
updatePolicy:
updateMode: "Recreate"
resourcePolicy:
containerPolicies:
- containerName: "*"
controlledResources: ["cpu", "memory"]
# -----------------
# gateway
# -------
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: web-gateway
namespace: nginx-gateway
spec:
gatewayClassName: nginx
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Same
# --------
helm upgrade -n kk-ns kk-mock1 kk-mock1/podinfo --version 6.11.2
# --------
```

View File

@@ -0,0 +1,114 @@
```bash
# storageclass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-sc
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: kubernetes.io/no-provisioner
reclaimPolicy: Retain # default value is Delete
volumeBindingMode: WaitForFirstConsumer
# -------------
# multipod deployment
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: logging-deployment
name: logging-deployment
namespace: logging-ns
spec:
replicas: 1
selector:
matchLabels:
app: logging-deployment
strategy: {}
template:
metadata:
labels:
app: logging-deployment
spec:
containers:
- command: ["/bin/sh", "-c"]
args:
- |
mkdir -p /var/log/app
while true; do
echo "Log entry" >> /var/log/app/app.log
sleep 5
done
image: busybox
name: busybox
volumeMounts:
- name: log-vol
mountPath: /var/log/app
- name: log-agent
image: busybox
command: ["/bin/sh", "-c"]
args:
- |
touch /var/log/app/app.log
tail -f /var/log/app/app.log
volumeMounts:
- name: log-vol
mountPath: /var/log/app
volumes:
- name: log-vol
emptyDir: {}
# -----------
kubectl create ingress webapp-ingress -n ingress-ns --rule="kodekloud-ingress.app/*=webapp-svc:80" --class=nginx -o yaml --dry-run > ingress.yaml
# --------
# create deployment, upgrade image
kubectl create deployment nginx-deploy --image=nginx:1.16 --replicas=1
kubectl set image deployment/nginx-deploy nginx=nginx:1.17
# --------
cat csr.yaml
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: john-developer # example
spec:
# This is an encoded CSR. Change this to the base64-encoded contents of myuser.csr
request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ1ZEQ0NBVHdDQVFBd0R6RU5NQXNHQTFVRUF3d0VhbTlvYmpDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRApnZ0VQQURDQ0FRb0NnZ0VCQU5BTEhVNlMxSVE1WWU4QkE1SW0vZlJlVlhaQ0QrUTUxT0NocmtWdXZ2Y0RFcERRCjZvQ2hjRXJoOUVYWlEwL0g4eEVOSkZoTzY1cyszLzVqRmRWZXRWY2RrZk1UVWJJdUtvS092R3ZWSlU4dWowWnQKUy9KRjRGQkQvOXdndXk1azYvY3hFSlF1cHMzLzBwTCtqUi9rK01SM0ZscWZCZG1WV1dVMmxmYm5TWElsQnFLMwova3RMcUhadG9oT2ZVN0hYR2gwUEY5YThCaXpmK0REdE8ya3Z1VFhMMHBJV3FyVkx6QUhPZVdId1phVXpmUmUvCnR1UjFncHF2a2dTNFJPTWdHeExucHZZcXgyV0cxbDNGaTBzTDVxM1F0dkhzelMrZjQwcWVzd2pnU0JQcTM1amQKR1hIakFJdnY4MFliSWxub0FTd0oyUzR0Z3NvNG4yMitZc2FkTXpVQ0F3RUFBYUFBTUEwR0NTcUdTSWIzRFFFQgpDd1VBQTRJQkFRQ01reFAxWElaWUhSUHNuVHdlRGUzTDIzeWhJLzVlOWltbzZrTWd0MVhxRXFNeVJvTUc4dStKCmlPNmFHVFpMSE5QbFVKR3pqSWpGR2RsUUtDMVpiOEUyUVpsOFhjNjA5SzlTOFlFVjg2Y0tNL2xxR1Q3OFp1blEKb1ljRWpwRUZYUHMwQkZMQlYwRnRVeDNVd2JWYmltMGErOEtWSjhFNllnSjcrb2JOaURNc0NpR21hK1J0K0hmRwpHcE1vaEdhQXRoSFhuSEtYbUk1VjNBbmtKM0c3am1rdThWY1ZBYkl3NWs2L1RUQjBZUnQxUXdIZGxMUGU1ZVc4ClBCYnB1bEpaMWxocWhxV01lblVIOU9UTVI4b0ZiazFzaVpSN2VKeU5GMWtWYlRkQWVLT2FsMVk3TlcxamY1Y0gKREhwcFBZRW84ZlJ1UE5GTXFCNTlxQkM4bEF5MGd0MlcKLS0tLS1FTkQgQ0VSVElGSUNBVEUgUkVRVUVTVC0tLS0tCg==
signerName: kubernetes.io/kube-apiserver-client
expirationSeconds: 86400 # one day
usages:
- client auth
kubectl certificate approve john
kubectl certificate approve john-developer
kubectl create role developer --verb=create,list,get,update,delete --resource=pods -n development
kubectl create rolebinding developer --role developer -n development --user=john
# ---------
kubectl run nginx-resolver --image=nginx
kubectl expose pod nginx-resolver --port=80 --name=nginx-resolver-service
k run test-dns --image=busybox:1.28 --rm -it --restart=Never \
-- nslookup nginx-resolver-service > /root/CKA/nginx.svc
k run test-dns --image=busybox:1.28 --rm -it --restart=Never -- nslookup 172-17-1-15.default.pod.cluster.local > /root/CKA/nginx.pod
# -----
# nginx-critical
k run nginx-critical --image=nginx -o yaml --dry-run > /etc/kubernetes/manifests/nginx-critical.yaml
# ------
# HPA
kubectl autoscale deployment backend-deployment --min=3 --max=15 --memory=65% --name backend-hpa -n backend

View File

@@ -0,0 +1,237 @@
# Task 1
```bash
sysctl net.ipv4.ip_forward=1
sysctl net.bridge.bridge-nf-call-iptables=1
# it is already persistent: /etc/sysctl.d/k8s.conf
```
# Task 2
```bash
kubectl create serviceaccount pvviewer
kubectl create clusterrole pvviewer-role --verb=list --resource=persistentVolumes
kubectl create clusterrolebinding pvviewer-role-binding --clusterrole=pvviewer-role --serviceaccount=default:pvviewer
k run pvviewer --image=redis -o yaml --dry-run > p.yaml
# edit -> add serviceAccount -> apply
```
# Task 3
```bash
cat sc.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: rancher-sc
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
```
# Task 4
```bash
kubectl create configmap app-config -n cm-namespace --from-litera
l=ENV=production --from-literal=LOG_LEVEL=info
k get deployments.apps -n cm-namespace cm-webapp -o yaml | yq .spec
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app: cm-webapp
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
app: cm-webapp
spec:
containers:
- env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
key: LOG_LEVEL
name: app-config
- name: ENV
valueFrom:
configMapKeyRef:
key: ENV
name: app-config
image: nginx
imagePullPolicy: Always
name: nginx
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
```
# Task 5
```bash
cat pc.yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: low-priority
value: 50000
```
# Task 6
```bash
cat np.yaml
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ingress-to-nptest
spec:
podSelector:
matchLabels:
run: np-test-1
ingress:
- ports:
- protocol: TCP
port: 80
policyTypes:
- Ingress
```
# Task 7
```bash
k taint node node01 kodekloud:NoSchedule
k run dev-redis --image redis:alpine
k run prod-redis --image redis:alpine -o yaml --dry-run > p7.yaml
cat p7.yaml
apiVersion: v1
kind: Pod
metadata:
labels:
run: prod-redis
name: prod-redis
spec:
containers:
- image: redis:alpine
name: prod-redis
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
tolerations:
- key: "kodekloud"
operator: "Exists"
effect: "NoSchedule"
```
# Task 8
```bash
# k get storageClasses
# fix accessMode
kubectl get pvc app-pvc -n storage-ns -o yaml > pvc.yaml
cat pvc.yaml | yq .spec
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
volumeMode: Filesystem
```
# Task 9
```bash
# fix port to api server in kubeconfig
```
# Task 10
```bash
k scale deployment nginx-deploy --replicas=3
# /etc/kubernetes/manifests/controller-manager ... typo in command
```
# Task 11
```bash
# hpa
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa
namespace: api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-deployment
minReplicas: 1
maxReplicas: 20
metrics:
- type: Pods
pods:
metric:
name: requests_per_second
target:
type: AverageValue
averageValue: "1000"
```
# Task 12
```bash
# httproute
cat httproute.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: web-route
spec:
parentRefs:
- name: web-gateway
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: web-service
port: 80
weight: 80
- name: web-service-v2
port: 80
weight: 20
```
# Task 13
```bash
helm install webpage-server-02 /root/new-version/
helm uninstall webpage-server-01
```
# Task 14
```bash
k get cm -n kube-system kubeadm-config -o yaml
echo "172.17.0.0/16" > /root/pod-cidr.txt
```

View File

@@ -0,0 +1,328 @@
# Task 2
Solve this question on: ssh cks8930
The Vulnerability Scanner trivy is installed on your main terminal. Use it to scan the following images for known CVEs:
nginx:1.16.1-alpine
k8s.gcr.io/kube-apiserver:v1.18.0
k8s.gcr.io/kube-controller-manager:v1.18.0
docker.io/weaveworks/weave-kube:2.7.0
Write all image names (including the tag, exactly as listed above) that don't contain the vulnerabilities CVE-2020-10878 or CVE-2020-1967 into /opt/course/2/good-images on cks8930.
# Task 3
Controlling Access to the Kubernetes API
Solve this question on: ssh cks8930
You received a list from the DevSecOps team which performed a security investigation of the cluster. The list states the following about the apiserver setup:
Accessible through a NodePort Service
Change the apiserver setup so that:
Only accessible through a ClusterIP Service
Use sudo -i to become root which may be required for this question
# Task 4
Configure Service Accounts for Pods Managing Service Accounts
Solve this question on: ssh cks5608
Update file /opt/course/4/stream-multiplex.yaml with the following changes:
Pods should have annotation token-lifetime with value 1200
ServiceAccount stream-multiplex should be used
Disable automounting of ServiceAccount tokens
The ServiceAccount token should be mounted at /var/run/secrets/custom/ with an expiration of 1200s
Create the Deployment and ensure it's running without errors.
# Task 5
Securing a Cluster
Solve this question on: ssh cks7262
You're asked to evaluate specific settings of the cluster against the CIS Benchmark recommendations. Use the kube-bench tool which is already installed on the nodes.
Connect to the worker node using ssh cks7262-node1 from cks7262.
On the controlplane node ensure (correct if necessary) that the CIS recommendations are set for:
The --profiling argument of the kube-controller-manager
The ownership of directory /var/lib/etcd
On the worker node ensure (correct if necessary) that the CIS recommendations are set for:
The permissions of the kubelet configuration /var/lib/kubelet/config.yaml
The --client-ca-file argument of the kubelet
Use sudo -i to become root which may be required for this question
# Task 6
Configure a Security Context
Solve this question on: ssh cks2546
The Deployment immutable-deployment in Namespace team-purple should run immutable, it's created from file /opt/course/6/immutable-deployment.yaml on cks2546. Even after a successful break-in, it shouldn't be possible for an attacker to modify the filesystem of the running container.
Modify the Deployment in a way that no processes inside the container can modify the local filesystem, only /tmp directory should be writable. Don't modify the Docker image.
Save the updated YAML under /opt/course/6/immutable-deployment-new.yaml on cks2546 and update the running Deployment.
# Task 7
Pod Security Standards Pod Security Admission
Solve this question on: ssh cks5608
Implement specific security policies in Namespace team-sepia.
Configure Pod Security Admission in mode audit for level baseline
Configure Pod Security Admission in mode warn for level restricted
Afterwards create the Pod from /opt/course/7/bad-pod.yaml and write any warnings or errors into /opt/course/7/bad-pod.log
# Task 8
Solve this question on: ssh cks4024
Docker containers on cks4024 should run more isolated from each other by disabling inter-container communication.
Add "icc": false to the Docker config and ensure the Docker daemon is using the updated settings
Create two Docker containers named container1 and container2 which should
have image nginx:1-alpine
restart always
keep running in the background
As result, the containers should not be able to ping each other on their IP addresses.
Run all Docker commands as root. Use sudo -i to become root
# Task 9
AppArmor
Solve this question on: ssh cks7262
Some containers need to run more secure and restricted. There is an existing AppArmor profile located at /opt/course/9/profile on cks7262 for this.
Install the AppArmor profile on node cks7262-node1.
Connect using ssh cks7262-node1 from cks7262
Add label security=apparmor to the node
Create a Deployment named apparmor in Namespace default with:
One replica of image nginx:1-alpine
NodeSelector for security=apparmor
Single container named c1 with the AppArmor profile enabled only for this container
The Pod might not run properly with the profile enabled. Write the logs of the Pod into /opt/course/9/logs on cks7262 so another team can work on getting the application running.
Use sudo -i to become root which may be required for this question
# Task 10
Runtime Class
Solve this question on: ssh cks7262
Team purple wants to run some of their workloads more securely. Worker node cks7262-node1 is already configured so that containerd supports the runsc/gvisor runtime.
Connect to the worker node using ssh cks7262-node1 from cks7262.
Create a RuntimeClass named gvisor with handler runsc
Create a Pod that uses the RuntimeClass. The Pod should be in Namespace team-purple, named gvisor-test and of image nginx:1-alpine
Ensure the Pod only ever runs on a node named cks7262-node1
Write the output of the dmesg command of the successfully started Pod into /opt/course/10/gvisor-test-dmesg on cks7262
# Task 11
Secrets Managing Secrets using kubectl
Solve this question on: ssh cks2546
There is Secret db-con in Namespace team-khaki-us-east-ad1. Update the password to 4c!29f_Ee2e and ensure all Pods currently using the Secret will work with the updated value.
Move Secret user-data from Namespace team-khaki-us-east-ad1 to team-khaki-us-east-ad2.
Convert ConfigMap app-data in Namespace team-khaki-us-east-ad1 to a Secret and delete the ConfigMap afterwards. Ensure all Pods that used the ConfigMap will continue to work and are now using the values from the Secret.
# Task 12
Admission Control in Kubernetes
Solve this question on: ssh cks4024
Team White created an ImagePolicyWebhook solution at /opt/course/12/webhook on cks4024 which needs to be enabled for the cluster. There is an existing and working webhook-backend Service in Namespace team-white which will be the ImagePolicyWebhook backend.
Create an AdmissionConfiguration at /opt/course/12/webhook/admission-config.yaml which contains the following ImagePolicyWebhook configuration in the same file:
imagePolicy:
kubeConfigFile: /etc/kubernetes/webhook/webhook.yaml
allowTTL: 10
denyTTL: 10
retryBackoff: 20
defaultAllow: true
Configure the apiserver to:
Mount /opt/course/12/webhook at /etc/kubernetes/webhook
Use the AdmissionConfiguration at path /etc/kubernetes/webhook/admission-config.yaml
Enable the ImagePolicyWebhook admission plugin
As result the ImagePolicyWebhook backend should prevent container images containing danger-danger from being used, any other image should still work.
Create a backup of /etc/kubernetes/manifests/kube-apiserver.yaml outside of /etc/kubernetes/manifests so you can revert back in case of issues
Use sudo -i to become root which may be required for this question
# Task 13
Cilium Documentation
Solve this question on: ssh cks8930
There is a metadata service available at http://192.168.100.21:9055 on which nodes can reach sensitive data. Access to this needs to be restricted from Pods.
In Namespace metadata-access create a CiliumNetworkPolicy named default to:
Allow egress to 0.0.0.0/0
Allow egress to Endpoints in the same Namespace
Allow egress to Endpoints in the kube-system Namespace
Deny egress to 192.168.100.21 on port 9055
There are existing plain Nginx Pods with open port 80 in the Namespace which can be used for testing but need to remain unchanged. Perform simple connectivity tests like:
k -n metadata-access exec POD_NAME -- curl URL
# Task 14
Encrypting Confidential Data at Rest
Solve this question on: ssh cks7262
An internal security audit requires to have secrets in the cluster encrypted. The team already created the needed EncryptionConfiguration at /etc/kubernetes/etcd/ec.yaml.
Write the non-encoded password that the aesgcm provider of that EncryptionConfiguration uses into /opt/course/14/password.txt
The Apiserver should mount /etc/kubernetes/etcd on the host to /etc/kubernetes/etcd inside the container
The Apiserver should use the EncryptionConfiguration from /etc/kubernetes/etcd/ec.yaml inside the container
All Secrets in Namespace team-magenta should be stored encrypted in ETCD
# Task 15
Ingress
Solve this question on: ssh cks2546
In Namespace team-pink there is an existing Nginx Ingress resource named secure which accepts two paths /app and /api which point to different ClusterIP Services.
From your main terminal you can connect to it using for example:
HTTP: curl -v http://secure-ingress.test:31080/app
HTTPS: curl -kv https://secure-ingress.test:31443/app
Right now it uses a default generated TLS certificate by the Nginx Ingress Controller.
You're asked to instead use the key and certificate provided at /opt/course/15/tls.key and /opt/course/15/tls.crt. As it's a self-signed certificate you need to use curl -k when connecting to it.
# Task 16
Falco Documentation
Solve this question on: ssh cks5608
Add two new Falco rules to /etc/falco/falco_rules.local.yaml:
Named Custom Rule 1 with priority WARNING. It should find all containers that access files on the host with prefix /etc/kubernetes in the full path. It should output logs as:
custom_rule_1 file={{FILEPATH}} container={{CONTAINER_ID}}
Named Custom Rule 2 with priority INFO. It should find all processes that perform kill syscalls. It should output logs as:
custom_rule_2 event_signal=%evt.arg.sig event_pid=%evt.arg.pid container={{CONTAINER_ID}}
Only create the new rules without additional macros or lists.
Run Falco with your implemented rules for at least 30 seconds and write the produced logs into /opt/course/16/logs.
# Task 17
Auditing
Solve this question on: ssh cks3477
Audit Logging has been enabled in the cluster with an Audit Policy located at /etc/kubernetes/audit/policy.yaml on cks3477.
Change the configuration so that only one backup of the logs is stored.
Alter the Policy in a way that it only stores logs:
From Secret resources, level Metadata
From "system:nodes" userGroups, level RequestResponse
After you update the Policy make sure to empty the log file so it only contains entries according to your changes, like using echo > /etc/kubernetes/audit/logs/audit.log.
You can use yq to render JSON more readable, like cat data.json | yq -p json -o json
Use sudo -i to become root which may be required for this question
# Preview 1
Preview Question 1
Using RBAC Authorization
Solve this question on: ssh cks3477
You're asked to implement some RBAC for user gianna:
There are existing cluster-level RBAC resources in place to, among other things, ensure that user gianna can never read Secret contents cluster-wide. Confirm this is correct or restrict the existing RBAC resources to ensure this.
In addition, create more RBAC resources to allow user gianna to create Pods and Deployments in Namespaces security, restricted and internal. It's likely the user will receive these exact permissions as well for other Namespaces in the future.
To test your RBAC you can:
Switch to the other context with:
k config use-context gianna@infra-prod
And afterwards switch back to the default context with:
k config use-context kubernetes-admin@kubernetes
# Preview 2
Preview Question 2
Auditing Managing Secrets using kubectl
Solve this question on: ssh cks3477
Namespace security contains five Secrets of type Opaque which can be considered highly confidential. The latest Incident-Prevention-Investigation revealed that ServiceAccount p.auster had too broad access to the cluster for some time. This SA should never have had access to any Secrets in that Namespace.
Find out which Secrets in Namespace security this SA did access by looking at the Audit Logs under /opt/course/p2/audit.log.
Change the password to any new string of only those Secrets that were accessed by this SA.
You can use jq to render json more readable, like cat data.json | jq
# Preview 3
Preview Question 3
Solve this question on: ssh cks8930
A security scan result shows that there is an unknown miner process running on one of the nodes in this cluster.
The report states that the process is listening on port 6666.
Kill the process and delete the binary.

View File