docs: add Kubernetes CKS study notes
This commit is contained in:
421
kubernetes/mock exams/claude-drill-pack-killer-sh-style.md
Normal file
421
kubernetes/mock exams/claude-drill-pack-killer-sh-style.md
Normal 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, 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 | | | | | |
|
||||
Reference in New Issue
Block a user