docs: add Kubernetes CKS study notes

This commit is contained in:
2026-08-04 23:18:45 +02:00
parent a6ee7a2b07
commit 91a1849009
57 changed files with 8313 additions and 0 deletions

View 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, 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 | | | | | |