Files
kodekloud-engineer/kubernetes/mock exams/claude-drill-pack-killer-sh-style.md

421 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 | | | | | |