16 KiB
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=falseunless noted.
K1 — NetworkPolicy: same-label, wrong-namespace trap — 8 min
SETUP
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
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
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
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
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
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
# 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:
/app(prefix) with headerX-Canaryexactlytrue→ Servicebeta:8080- all other
/app→legacy:8080 /appwithX-Canary: trueAND query paramdebug=1must ALSO reachbeta— third rule or covered already? Justify in one sentence.
VALIDATE (spec-level — dummy class, no dataplane)
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
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
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
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:
- Sets namespace
drill-prodon everything. - Adds name prefix
prod-to every resource. - Adds common label
env: prod— must land on the Deployment's metadata, its pod template, and its selector/Service selector (labels + selectors stay consistent). - Bumps the image to
nginx:1.27(use theimagestransformer, not a patch). - Scales replicas to 3 (use the
replicastransformer, not a patch). - Extends the
portal-configConfigMap withLOG_LEVEL=debugwhile keepingAPP_MODE=basic— this must be a generator merge, not a redefinition. - Adds a new
portal-secretSecret generator (literalAPI_KEY=s3cr3t) and wires it into the container'senvFromvia 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. - Adds container resource limits
cpu: 200m,memory: 128Mivia the same or a second JSON 6902 patch (resourcesisn't set in base — the op mustadd, notreplace).
Create namespace drill-prod, apply the overlay with -k.
VALIDATE
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
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
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
- Grant SA
auditorpermission to get and list ConfigMaps indrill-rbaconly — imperative commands only, no YAML files. - Prove with
kubectl auth can-i(positive AND negative: it must NOT list secrets). - Exec into
clientand curl the ConfigMaps list endpoint with the mounted token — capture the HTTP response kind. - Curl the Secrets endpoint the same way — capture the 403.
VALIDATE
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
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
- Create deployment
batch-runner(imagebusybox, commandsleep 3600, 2 replicas) in new nsdrill-schedthat runs only ondrills-worker— it must tolerate the taint AND be constrained to that node via its label (both mechanisms, and know why one alone is insufficient). - Create pod
web-pin(nginx) that must land ondrills-worker2using nodeAffinity (no nodeName).
VALIDATE
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)
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
k apply -f - <<'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: {name: drill-manual}
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
EOF
TASK
- Create PV
pv-drill: 1Gi,ReadWriteOnce, storageClassNamedrill-manual, hostPath/tmp/pv-drill. - Create PVC
pvc-drillin new nsdrill-store: request 500Mi, same class, RWO. - Explain (one sentence) why the PVC stays
Pendingright now — this is not an error. - Create pod
store-user(nginx) mounting the PVC at/data; confirm the bind flips.
VALIDATE
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
- In new ns
drill-side, create deploymenttracked-app(1 replica): main containerapp(busybox) appends the date to/var/log/app/beat.logevery 5s; native sidecartail-agent(busybox) — init container withrestartPolicy: Always— tails that file. Shared emptyDir at/var/log/app. - One command: print for every pod in
drill-sidethe columnsNAME,QOS,NODE, sorted by name, written to/tmp/drill-side.txt.
VALIDATE
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)
- Correct end state (validation output matches expected)
- Path efficiency — generator/patch/docs-copy vs hand-written YAML
- Verification discipline — did you run VALIDATE before declaring done
- Time vs budget
- 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 |