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

16 KiB
Raw Blame History

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

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:

  1. /app (prefix) with header X-Canary exactly true → Service beta:8080
  2. all other /applegacy: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)

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:

  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

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

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

  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

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

  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

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

  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

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

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