# 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 --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 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, 1–30). 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 | | | | | |