docs: add Kubernetes CKS study notes
This commit is contained in:
264
kubernetes/mock exams/cka-exam-pack-1-by-claude.md
Normal file
264
kubernetes/mock exams/cka-exam-pack-1-by-claude.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# CKA Mock Exam Pack v1 — full 120-minute simulation
|
||||
|
||||
> **Format-matched to the real thing:** 17 weighted tasks, 120 minutes, 66% to pass.
|
||||
> Domain distribution ≈ real curriculum: Troubleshooting ~30%, Cluster Architecture ~25%, Services & Networking ~20%, Workloads & Scheduling ~15%, Storage ~10%.
|
||||
> Range: kind cluster `drills` (1 CP + 2 workers, Calico, Gateway API CRDs, metrics-server, helm).
|
||||
>
|
||||
> **Kind-isms (read once, then forget):** the real exam says `ssh nodeX` — here it's `docker exec -it drills-worker bash` (or `drills-worker2`, `drills-control-plane`). Real exam switches kubectl contexts per task — here it's one context; the discipline you're simulating is *reading the target of every task carefully*.
|
||||
|
||||
---
|
||||
|
||||
## EXAM PROTOCOL
|
||||
|
||||
1. Run the entire **MASTER SETUP** below. Do not read it — it contains spoilers (it breaks things on purpose). Pipe it to a file and execute blind:
|
||||
copy the block into `/tmp/setup.sh`, then `bash /tmp/setup.sh > /tmp/setup.log 2>&1`.
|
||||
2. Set a hard 120:00 timer. No pauses, no notes, no Claude.
|
||||
3. Solve in any order. Flag anything stuck >8 min and move on. Reserve the last 10 minutes for a verification lap.
|
||||
4. When the timer dies: run the **VALIDATION** blocks, fill the score sheet, compute your percentage from the weights.
|
||||
5. Paste score sheet + your command history to Claude for the autopsy.
|
||||
|
||||
---
|
||||
|
||||
## MASTER SETUP (do not read — paste and run)
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set +e
|
||||
# --- namespaces
|
||||
for ns in ex-neptune ex-mars ex-venus ex-pluto ex-saturn ex-mercury ex-titan ex-io ex-rescue; do kubectl create ns $ns; done
|
||||
|
||||
# Q1: kill kubelet on worker2
|
||||
docker exec drills-worker2 systemctl stop kubelet
|
||||
|
||||
# Q5: broken deployment — missing CM + bad image tag
|
||||
kubectl -n ex-neptune create deploy web-portal --image=nginx:1.99-fake --replicas=2
|
||||
kubectl -n ex-neptune set env deploy/web-portal --from=configmap/portal-config 2>/dev/null
|
||||
kubectl -n ex-neptune patch deploy web-portal --type=json -p='[{"op":"add","path":"/spec/template/spec/containers/0/envFrom","value":[{"configMapRef":{"name":"portal-config"}}]}]'
|
||||
|
||||
# Q6: service with selector mismatch
|
||||
kubectl -n ex-mars create deploy api-backend --image=nginx --replicas=2
|
||||
kubectl -n ex-mars label deploy api-backend app=api-backend --overwrite
|
||||
kubectl -n ex-mars apply -f - <<'EOF'
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata: {name: api-svc, namespace: ex-mars}
|
||||
spec:
|
||||
selector: {app: api-backent}
|
||||
ports: [{port: 80, targetPort: 80}]
|
||||
EOF
|
||||
|
||||
# Q16: crashing pod
|
||||
kubectl -n ex-rescue run data-proc --image=busybox --restart=Never -- sh -c 'echo "FATAL: config /etc/proc/settings.ini not found" >&2; exit 1'
|
||||
|
||||
# Q17: broken static pod on worker
|
||||
docker exec drills-worker bash -c 'mkdir -p /etc/kubernetes/manifests && cat > /etc/kubernetes/manifests/edge-cache.yaml <<EOF
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata: {name: edge-cache}
|
||||
spec:
|
||||
containers:
|
||||
- name: cache
|
||||
image: redis:7-alpinee
|
||||
ports: [{containerPort: 6379}]
|
||||
EOF'
|
||||
|
||||
# Q4: gateway for httproute task
|
||||
kubectl apply -f - <<'EOF'
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: GatewayClass
|
||||
metadata: {name: exam-class}
|
||||
spec: {controllerName: example.com/exam}
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: Gateway
|
||||
metadata: {name: main-gate, namespace: ex-venus}
|
||||
spec:
|
||||
gatewayClassName: exam-class
|
||||
listeners: [{name: http, port: 80, protocol: HTTP}]
|
||||
EOF
|
||||
kubectl -n ex-venus create deploy web-v1 --image=hashicorp/http-echo -- /http-echo -text=v1 -listen=:5678
|
||||
kubectl -n ex-venus create deploy web-v2 --image=hashicorp/http-echo -- /http-echo -text=v2 -listen=:5678
|
||||
kubectl -n ex-venus expose deploy web-v1 --port=5678
|
||||
kubectl -n ex-venus expose deploy web-v2 --port=5678
|
||||
|
||||
# Q3: netpol targets
|
||||
kubectl -n ex-pluto run frontend --image=nginx --labels=role=frontend --port=80
|
||||
kubectl -n ex-pluto run backend --image=nginx --labels=role=backend --port=80
|
||||
kubectl -n ex-pluto run cache --image=nginx --labels=role=cache --port=80
|
||||
|
||||
# Q7: rbac targets
|
||||
kubectl -n ex-saturn create sa deploy-bot
|
||||
|
||||
# Q11: rollout history to inspect
|
||||
kubectl -n ex-mercury create deploy release-app --image=nginx:1.25
|
||||
kubectl -n ex-mercury set image deploy/release-app nginx=nginx:1.26
|
||||
kubectl -n ex-mercury set image deploy/release-app nginx=nginx:1.27-bogus
|
||||
|
||||
# Q12: scheduling
|
||||
kubectl taint node drills-worker tier=critical:NoSchedule --overwrite
|
||||
kubectl label node drills-worker zone=east --overwrite
|
||||
kubectl label node drills-worker2 zone=west --overwrite
|
||||
|
||||
# Q13/Q14: storage
|
||||
kubectl apply -f - <<'EOF'
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata: {name: local-manual}
|
||||
provisioner: kubernetes.io/no-provisioner
|
||||
volumeBindingMode: WaitForFirstConsumer
|
||||
EOF
|
||||
docker exec drills-worker mkdir -p /opt/vol-alpha
|
||||
kubectl -n ex-io create deploy metrics-writer --image=busybox -- sleep 3600
|
||||
echo "SETUP DONE"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## THE EXAM — 120:00 starts now
|
||||
|
||||
### Q1 — 8% — Troubleshooting
|
||||
Node `drills-worker2` is `NotReady`. Find the cause, fix it, make the node `Ready` again. Write the name of the failed component into `/tmp/exam/q1-component.txt`.
|
||||
|
||||
### Q2 — 8% — Cluster Architecture
|
||||
Create a snapshot of the cluster's etcd database and save it **inside the control-plane node** at `/root/etcd-backup.db`. Then verify the snapshot and write the number of keys/revision info into `/tmp/exam/q2-status.txt` on your host.
|
||||
*(Hint allowed by the real exam's phrasing: certs live where kubeadm puts them. Enter the CP with `docker exec -it drills-control-plane bash`.)*
|
||||
|
||||
### Q3 — 6% — Services & Networking
|
||||
In `ex-pluto`: Pods `role=backend` must accept ingress **only** from Pods `role=frontend` in the same namespace on port 80. Traffic from `role=cache` (and everything else) must be denied. Name the policy `backend-shield`. Egress stays open.
|
||||
|
||||
### Q4 — 7% — Services & Networking
|
||||
In `ex-venus`, attached to the existing Gateway `main-gate`, create HTTPRoute `release-route`: all requests to path prefix `/` go to `web-v1:5678`, **except** requests with header `X-Version: v2` (exact) which go to `web-v2:5678`. Spec-level correctness counts (no dataplane on this range).
|
||||
|
||||
### Q5 — 7% — Troubleshooting
|
||||
Deployment `web-portal` in `ex-neptune` has 0/2 ready. There are **two independent faults**. Fix both. Desired end state: 2/2 Ready, env sourced from a ConfigMap named `portal-config` containing key `MODE=production`.
|
||||
|
||||
### Q6 — 6% — Troubleshooting
|
||||
Service `api-svc` in `ex-mars` returns no endpoints. Diagnose and repair **the service** (the deployment is correct). Prove endpoints exist afterward.
|
||||
|
||||
### Q7 — 7% — Cluster Architecture
|
||||
In `ex-saturn`, using **imperative commands only**: allow ServiceAccount `deploy-bot` to create, list, and delete Deployments in `ex-saturn` — nothing else, namespace-scoped. Verify with `kubectl auth can-i` (one positive, one negative check) and save both outputs to `/tmp/exam/q7-cani.txt`.
|
||||
|
||||
### Q8 — 6% — Cluster Architecture
|
||||
Using Helm: install chart `bitnami/redis` as release `cache-layer` in namespace `ex-titan`, chart version **one minor behind latest**, with `architecture=standalone` and auth disabled (`auth.enabled=false`) set via CLI. Then upgrade to the latest chart version preserving both overrides. Record `helm history` output to `/tmp/exam/q8-history.txt`.
|
||||
|
||||
### Q9 — 4% — Cluster Architecture
|
||||
A base kustomization exists at `/tmp/drill-kz/base` (from drill pack v1; recreate it if you cleaned it). Create overlay `/tmp/drill-kz/overlays/exam` that deploys into namespace `ex-titan` with name prefix `exam-` and image `nginx:1.27`. Apply it with `-k`.
|
||||
|
||||
### Q10 — 5% — Workloads & Scheduling
|
||||
Create HPA `writer-hpa` in `ex-io` for deployment `metrics-writer`: min 1, max 4, target 70% average CPU. Scale-down stabilization window 240s. (The deployment lacks CPU requests — part of the task is making the HPA functional.)
|
||||
|
||||
### Q11 — 5% — Workloads & Scheduling
|
||||
Deployment `release-app` in `ex-mercury` is stuck mid-rollout on a bad image. Roll it back to the **previous working revision**, then record: current image and the revision number you rolled back to → `/tmp/exam/q11-rollback.txt`.
|
||||
|
||||
### Q12 — 5% — Workloads & Scheduling
|
||||
Create deployment `edge-daemon` (image `busybox`, `sleep 3600`, 2 replicas) in namespace `ex-io` that runs **only on `drills-worker`**: tolerate its taint and pin via nodeAffinity on the `zone=east` label. Both replicas must land there.
|
||||
|
||||
### Q13 — 5% — Storage
|
||||
Create PV `vol-alpha`: 2Gi, RWO, storageClassName `local-manual`, hostPath `/opt/vol-alpha`. Create PVC `claim-alpha` in `ex-io` requesting 1Gi, same class. Explain in one line inside `/tmp/exam/q13-why.txt` why the PVC is Pending at this point. Then mount it in a pod `vol-user` (nginx) at `/data` and confirm Bound.
|
||||
|
||||
### Q14 — 5% — Storage
|
||||
Make `local-manual` the **default** StorageClass of the cluster. Then create PVC `claim-default` in `ex-io` with **no storageClassName** and prove (one command's output to `/tmp/exam/q14-proof.txt`) that it was assigned `local-manual` automatically.
|
||||
|
||||
### Q15 — 5% — Services & Networking
|
||||
In `ex-mars`, create an Ingress `api-ingress` (imperative generator allowed): host `api.exam.local`, path `/v1` (Prefix) → service `api-svc:80`, ingressClassName `nginx`. The controller isn't installed — spec-level correctness counts. Ensure pathType is explicitly `Prefix`.
|
||||
|
||||
### Q16 — 6% — Troubleshooting
|
||||
Pod `data-proc` in `ex-rescue` is failing. Save **its complete logs** (the error line included) to `/tmp/exam/q16-logs.txt`, then recreate the pod so it runs successfully with the same name and image, sleeping 1 hour instead of crashing.
|
||||
|
||||
### Q17 — 5% — Troubleshooting
|
||||
A static pod `edge-cache` was configured on node `drills-worker` but never appears in `kubectl get pods`. Enter the node, find the fault, fix it, and get the mirror pod visible and Running in the `default` namespace.
|
||||
|
||||
---
|
||||
|
||||
## VALIDATION (run only after the timer — expected outputs in comments)
|
||||
|
||||
```bash
|
||||
EXAM_DIR="${EXAM_DIR:-./exam}"
|
||||
mkdir -p "$EXAM_DIR"
|
||||
# Q1
|
||||
kubectl get node drills-worker2 # Ready
|
||||
cat "$EXAM_DIR/q1-component.txt" # kubelet
|
||||
# Q2
|
||||
docker exec drills-control-plane ls -la /root/etcd-backup.db # exists, >1MB
|
||||
cat "$EXAM_DIR/q2-status.txt" # snapshot status table/json
|
||||
# Q3
|
||||
PIP=$(kubectl -n ex-pluto get pod backend -o jsonpath='{.status.podIP}')
|
||||
kubectl -n ex-pluto exec frontend -- curl -s -m 2 $PIP | head -1 # HTML (allowed)
|
||||
kubectl -n ex-pluto exec cache -- curl -s -m 2 $PIP; echo EXIT=$? # EXIT=28 (blocked)
|
||||
# Q4
|
||||
kubectl -n ex-venus get httproute release-route -o yaml
|
||||
# header match in same rule-entry as its path; default rule present; parentRef=main-gate
|
||||
# Q5
|
||||
kubectl -n ex-neptune get deploy web-portal # 2/2
|
||||
kubectl -n ex-neptune get cm portal-config -o jsonpath='{.data.MODE}' # production
|
||||
# Q6
|
||||
kubectl -n ex-mars get endpoints api-svc # two pod IPs
|
||||
# Q7
|
||||
cat "$EXAM_DIR/q7-cani.txt" # yes + no
|
||||
kubectl auth can-i delete deployments --as=system:serviceaccount:ex-saturn:deploy-bot -n ex-saturn # yes
|
||||
kubectl auth can-i create pods --as=system:serviceaccount:ex-saturn:deploy-bot -n ex-saturn # no
|
||||
# Q8
|
||||
helm list -n ex-titan # cache-layer, latest chart
|
||||
helm get values cache-layer -n ex-titan # architecture: standalone, auth.enabled: false
|
||||
cat "$EXAM_DIR/q8-history.txt" # rev1 + rev2
|
||||
# Q9
|
||||
kubectl -n ex-titan get deploy exam-portal -o jsonpath='{.spec.template.spec.containers[0].image}' # nginx:1.27
|
||||
# Q10
|
||||
kubectl -n ex-io get hpa writer-hpa -o yaml | grep -E 'averageUtilization|stabilizationWindowSeconds'
|
||||
# 70 + 240 under scaleDown; deploy now has cpu requests
|
||||
# Q11
|
||||
kubectl -n ex-mercury get deploy release-app -o jsonpath='{.spec.template.spec.containers[0].image}' # nginx:1.26
|
||||
cat "$EXAM_DIR/q11-rollback.txt"
|
||||
# Q12
|
||||
kubectl -n ex-io get pods -l app=edge-daemon -o wide # both on drills-worker
|
||||
# Q13
|
||||
kubectl -n ex-io get pvc claim-alpha # Bound
|
||||
cat "$EXAM_DIR/q13-why.txt" # WaitForFirstConsumer explanation
|
||||
# Q14
|
||||
kubectl get sc local-manual -o jsonpath='{.metadata.annotations.storageclass\.kubernetes\.io/is-default-class}' # true
|
||||
cat "$EXAM_DIR/q14-proof.txt" # claim-default → local-manual
|
||||
# Q15
|
||||
kubectl -n ex-mars get ingress api-ingress -o jsonpath='{.spec.rules[0].http.paths[0].pathType}' # Prefix
|
||||
# Q16
|
||||
grep FATAL "$EXAM_DIR/q16-logs.txt" # the error line
|
||||
kubectl -n ex-rescue get pod data-proc # Running
|
||||
# Q17
|
||||
kubectl get pod edge-cache-drills-worker # Running
|
||||
```
|
||||
|
||||
> **Configurable path:** the validation block reads/writes under `$EXAM_DIR` (defaults to `/tmp/exam`). Override by exporting it before running, e.g. `EXAM_DIR=/tmp/exam-attempt2 bash -c '...'` or `export EXAM_DIR=/tmp/exam-attempt2` beforehand. The exam task descriptions above still reference the literal `/tmp/exam/...` paths — either keep writing there during the exam, or export the same `EXAM_DIR` value before starting the timer and use `$EXAM_DIR/...` for every task's output path too.
|
||||
|
||||
## SCORE SHEET
|
||||
| Q | Weight | Domain | Pass? | Time |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 8 | TS | | |
|
||||
| 2 | 8 | CA | | |
|
||||
| 3 | 6 | SN | | |
|
||||
| 4 | 7 | SN | | |
|
||||
| 5 | 7 | TS | | |
|
||||
| 6 | 6 | TS | | |
|
||||
| 7 | 7 | CA | | |
|
||||
| 8 | 6 | CA | | |
|
||||
| 9 | 4 | CA | | |
|
||||
| 10 | 5 | WS | | |
|
||||
| 11 | 5 | WS | | |
|
||||
| 12 | 5 | WS | | |
|
||||
| 13 | 5 | ST | | |
|
||||
| 14 | 5 | ST | | |
|
||||
| 15 | 5 | SN | | |
|
||||
| 16 | 6 | TS | | |
|
||||
| 17 | 5 | TS | | |
|
||||
|
||||
**Score = sum of passed weights. Pass line: 66.**
|
||||
|
||||
## RESET (between attempts)
|
||||
```bash
|
||||
for ns in ex-neptune ex-mars ex-venus ex-pluto ex-saturn ex-mercury ex-titan ex-io ex-rescue; do kubectl delete ns $ns --wait=false; done
|
||||
kubectl delete gatewayclass exam-class; kubectl delete pv vol-alpha
|
||||
kubectl taint node drills-worker tier=critical:NoSchedule- 2>/dev/null
|
||||
kubectl label node drills-worker zone- ; kubectl label node drills-worker2 zone-
|
||||
kubectl patch sc local-manual -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
|
||||
docker exec drills-worker rm -f /etc/kubernetes/manifests/edge-cache.yaml
|
||||
docker exec drills-worker2 systemctl start kubelet 2>/dev/null
|
||||
kubectl delete pod edge-cache-drills-worker --force 2>/dev/null
|
||||
```
|
||||
369
kubernetes/mock exams/cka-exam-pack-1-solutions.md
Normal file
369
kubernetes/mock exams/cka-exam-pack-1-solutions.md
Normal file
@@ -0,0 +1,369 @@
|
||||
# CKA Mock Exam Pack v1 — SOLUTIONS
|
||||
|
||||
> ⚠️ **Do not open before a timed attempt.** killer.sh rules: solve first, autopsy second. Reading solutions cold converts a diagnostic into trivia.
|
||||
> Each solution: the fastest correct path → why it works → the planted trap → what a grader validates.
|
||||
|
||||
---
|
||||
|
||||
## Q1 — NotReady node (8%)
|
||||
|
||||
```bash
|
||||
kubectl get nodes # drills-worker2 NotReady
|
||||
docker exec -it drills-worker2 bash # real exam: ssh node
|
||||
systemctl status kubelet # inactive (dead)
|
||||
systemctl start kubelet && systemctl enable kubelet
|
||||
exit
|
||||
kubectl get node drills-worker2 # Ready within ~30s
|
||||
mkdir -p /tmp/exam && echo kubelet > /tmp/exam/q1-component.txt
|
||||
```
|
||||
|
||||
**Why:** NotReady = node agent not reporting. Diagnostic ladder on the node: `systemctl status kubelet` → if running, `journalctl -u kubelet -f` for cert/config/CNI errors → container runtime (`systemctl status containerd`). Here it's simply stopped — the most common exam variant.
|
||||
**Trap:** none beyond forgetting `enable` (real exam expects the fix to survive reboot; graders have failed people on start-without-enable).
|
||||
**Grader checks:** node Ready + the component name in the file.
|
||||
|
||||
---
|
||||
|
||||
## Q2 — etcd snapshot (8%)
|
||||
|
||||
```bash
|
||||
docker exec -it drills-control-plane bash
|
||||
ETCDCTL_API=3 etcdctl snapshot save /root/etcd-backup.db \
|
||||
--endpoints=https://127.0.0.1:2379 \
|
||||
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
|
||||
--cert=/etc/kubernetes/pki/etcd/server.crt \
|
||||
--key=/etc/kubernetes/pki/etcd/server.key
|
||||
etcdctl snapshot status /root/etcd-backup.db -w table
|
||||
exit
|
||||
docker exec drills-control-plane etcdctl snapshot status /root/etcd-backup.db -w table > /tmp/exam/q2-status.txt
|
||||
```
|
||||
|
||||
If `etcdctl` isn't on the node's PATH, run it inside the etcd pod instead:
|
||||
`kubectl -n kube-system exec etcd-drills-control-plane -- sh -c 'ETCDCTL_API=3 etcdctl snapshot save ...'` — but then the file lands in the pod's filesystem; since etcd's static pod hostPath-mounts `/etc/kubernetes/pki/etcd`, save to a hostPath-mounted dir or copy out. Node-local etcdctl is cleaner when present.
|
||||
|
||||
**Why those flags:** etcd serves TLS with client-cert auth; the cert paths are the kubeadm defaults — you don't memorize them, you read them live: `grep -E 'cert|key|ca' /etc/kubernetes/manifests/etcd.yaml`. That grep is the actual skill.
|
||||
**Trap:** using `--cert=.../apiserver-etcd-client.crt` also works (it's a valid client cert) — but pointing at the *apiserver's serving* cert doesn't. When in doubt, read etcd.yaml's own `--cert-file/--key-file` lines.
|
||||
**Grader checks:** file exists on the CP node, `snapshot status` output captured. Real exam restore variant: `etcdctl snapshot restore --data-dir=/var/lib/etcd-restore`, then edit etcd.yaml's hostPath to the new dir; kubelet restarts the static pod.
|
||||
|
||||
---
|
||||
|
||||
## Q3 — NetworkPolicy (6%)
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: backend-shield
|
||||
namespace: ex-pluto
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels: {role: backend}
|
||||
policyTypes: [Ingress] # Ingress only — egress stays open per task
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels: {role: frontend}
|
||||
ports:
|
||||
- {protocol: TCP, port: 80}
|
||||
```
|
||||
|
||||
**Why:** listing only `Ingress` in policyTypes restricts ingress and leaves egress untouched — adding `Egress` with no egress rules would silently deny all outbound, violating the task. `from.podSelector` without a namespaceSelector = same-namespace pods only, which is exactly the requirement.
|
||||
**Trap:** cache pod has a label too (`role=cache`) — but selection is allowlist-based: not matching `from` = denied. The drill is trusting deny-by-default once any policy selects the pod.
|
||||
**Grader checks:** functional — curl from frontend succeeds, from cache times out. Always run both directions yourself; a policy selecting zero pods passes the positive test and fails the negative one.
|
||||
|
||||
---
|
||||
|
||||
## Q4 — HTTPRoute with header exception (7%)
|
||||
|
||||
```yaml
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: release-route
|
||||
namespace: ex-venus
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: main-gate
|
||||
rules:
|
||||
- matches: # specific rule first: path AND header
|
||||
- path: {type: PathPrefix, value: /}
|
||||
headers:
|
||||
- {name: X-Version, type: Exact, value: v2}
|
||||
backendRefs:
|
||||
- {name: web-v2, port: 5678}
|
||||
- matches: # catch-all
|
||||
- path: {type: PathPrefix, value: /}
|
||||
backendRefs:
|
||||
- {name: web-v1, port: 5678}
|
||||
```
|
||||
|
||||
**Why:** conditions inside ONE match entry are ANDed (path ∧ header); separate entries in the matches list are ORed. The header rule must be its own rule with both conditions co-located. Spec precedence already prefers more-specific matches (header count breaks path-length ties), but ordering specific-first documents intent and defends against lax implementations.
|
||||
**Trap:** putting header and path in two match entries → OR → every request matches the "v2" rule.
|
||||
**Grader checks:** parentRef=main-gate, AND-structure, both backends with correct ports.
|
||||
|
||||
---
|
||||
|
||||
## Q5 — two-fault deployment (7%)
|
||||
|
||||
```bash
|
||||
kubectl -n ex-neptune describe pod -l app=web-portal # fault A: ImagePullBackOff nginx:1.99-fake
|
||||
# fault B: CreateContainerConfigError — configmap "portal-config" not found
|
||||
kubectl -n ex-neptune create cm portal-config --from-literal=MODE=production
|
||||
kubectl -n ex-neptune set image deploy/web-portal nginx-1-99-fake=nginx:1.27
|
||||
# (container name from: kubectl -n ex-neptune get deploy web-portal -o jsonpath='{.spec.template.spec.containers[0].name}')
|
||||
kubectl -n ex-neptune rollout status deploy/web-portal # 2/2
|
||||
```
|
||||
|
||||
**Why:** the task says two *independent* faults — fixing one and declaring victory is the trap. Image fix alone → pods still stuck on the missing CM; CM alone → still ImagePullBackOff. `describe` shows both symptoms at once if you read the whole Events section.
|
||||
**Trap:** the container is named after the bogus image by the generator — `set image deploy/web-portal *=nginx:1.27` (wildcard) sidesteps needing the name.
|
||||
**Grader checks:** 2/2 Ready + CM exists with MODE=production + envFrom intact.
|
||||
|
||||
---
|
||||
|
||||
## Q6 — Service without endpoints (6%)
|
||||
|
||||
```bash
|
||||
kubectl -n ex-mars get endpoints api-svc # <none>
|
||||
kubectl -n ex-mars get svc api-svc -o yaml | grep -A2 selector # app: api-backent ← typo
|
||||
kubectl -n ex-mars get pods --show-labels # pods carry app=api-backend
|
||||
kubectl -n ex-mars patch svc api-svc -p '{"spec":{"selector":{"app":"api-backend"}}}'
|
||||
kubectl -n ex-mars get endpoints api-svc # two IPs
|
||||
```
|
||||
|
||||
**Why:** empty Endpoints with healthy pods = selector/label mismatch ~90% of the time (the rest: no pods Ready, or wrong targetPort). The diff-the-strings discipline (`api-backent` vs `api-backend`) is the whole question.
|
||||
**Trap:** task says fix *the service* — relabeling the pods "works" functionally but violates the instruction and may fail the check.
|
||||
**Grader checks:** Endpoints populated; deployment untouched.
|
||||
|
||||
---
|
||||
|
||||
## Q7 — imperative RBAC (7%)
|
||||
|
||||
```bash
|
||||
kubectl -n ex-saturn create role deploy-manager \
|
||||
--verb=create --verb=list --verb=delete --resource=deployments
|
||||
kubectl -n ex-saturn create rolebinding deploy-bot-binding \
|
||||
--role=deploy-manager --serviceaccount=ex-saturn:deploy-bot
|
||||
kubectl auth can-i create deployments --as=system:serviceaccount:ex-saturn:deploy-bot -n ex-saturn > /tmp/exam/q7-cani.txt # yes
|
||||
kubectl auth can-i create pods --as=system:serviceaccount:ex-saturn:deploy-bot -n ex-saturn >> /tmp/exam/q7-cani.txt # no
|
||||
```
|
||||
|
||||
**Why:** Role (not ClusterRole) = namespace-scoped as demanded; `--serviceaccount=ns:name` is the binding syntax people forget (NOT `--user`). Identity string for can-i: `system:serviceaccount:<ns>:<name>`.
|
||||
**Trap:** "nothing else" — resist adding get/watch out of habit; graders sometimes diff the verb list.
|
||||
**Grader checks:** the two can-i outcomes, role shape.
|
||||
|
||||
---
|
||||
|
||||
## Q8 — Helm pin → upgrade keeping values (6%)
|
||||
|
||||
```bash
|
||||
helm search repo bitnami/redis --versions | head -5 # note latest (e.g. 21.x.y) and one minor back (21.(x-1).z or 20.x)
|
||||
helm install cache-layer bitnami/redis -n ex-titan \
|
||||
--version <one-minor-back> \
|
||||
--set architecture=standalone --set auth.enabled=false
|
||||
helm upgrade cache-layer bitnami/redis -n ex-titan \
|
||||
--version <latest> --reuse-values
|
||||
helm get values cache-layer -n ex-titan # both overrides intact
|
||||
helm history cache-layer -n ex-titan > /tmp/exam/q8-history.txt
|
||||
```
|
||||
|
||||
**Why:** `--reuse-values` is the "keep my overrides" flag — the planted trap. Bare `helm upgrade` resets to chart defaults + whatever `--set` you pass *now*; people assume values persist. (Equally valid: repeat both `--set` flags on upgrade — explicit beats clever; `--reuse-values` has sharp edges when combined with new `--set`s, merging old+new.)
|
||||
**Grader checks:** history shows 2 revisions, deployed chart = latest, values retain both overrides.
|
||||
|
||||
---
|
||||
|
||||
## Q9 — Kustomize overlay (4%)
|
||||
|
||||
`/tmp/drill-kz/overlays/exam/kustomization.yaml`:
|
||||
```yaml
|
||||
resources: [../../base]
|
||||
namespace: ex-titan
|
||||
namePrefix: exam-
|
||||
images:
|
||||
- name: nginx
|
||||
newTag: "1.27"
|
||||
```
|
||||
```bash
|
||||
kubectl kustomize /tmp/drill-kz/overlays/exam # eyeball first — always render before apply
|
||||
kubectl apply -k /tmp/drill-kz/overlays/exam
|
||||
```
|
||||
|
||||
**Why:** the `images` transformer rewrites by image *name* regardless of the tag in base — no patch file needed for a tag change. `namePrefix` renames the deployment to `exam-portal` (validation greps that name — a hint hidden in the pack).
|
||||
**Trap:** writing a strategicMerge patch for something transformers do in 3 lines — works, but slow.
|
||||
|
||||
---
|
||||
|
||||
## Q10 — HPA needing requests (5%)
|
||||
|
||||
```bash
|
||||
kubectl -n ex-io set resources deploy metrics-writer --requests=cpu=100m # the hidden half
|
||||
kubectl -n ex-io autoscale deploy metrics-writer --name=writer-hpa \
|
||||
--min=1 --max=4 --cpu-percent=70 --dry-run=client -o yaml > /tmp/hpa.yaml
|
||||
# edit: ensure autoscaling/v2 shape, add:
|
||||
# behavior:
|
||||
# scaleDown:
|
||||
# stabilizationWindowSeconds: 240
|
||||
kubectl apply -f /tmp/hpa.yaml
|
||||
```
|
||||
|
||||
Final spec core:
|
||||
```yaml
|
||||
apiVersion: autoscaling/v2
|
||||
spec:
|
||||
scaleTargetRef: {apiVersion: apps/v1, kind: Deployment, name: metrics-writer}
|
||||
minReplicas: 1
|
||||
maxReplicas: 4
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource: {name: cpu, target: {type: Utilization, averageUtilization: 70}}
|
||||
behavior:
|
||||
scaleDown:
|
||||
stabilizationWindowSeconds: 240
|
||||
```
|
||||
|
||||
**Why:** Utilization = percentage *of requests* — no requests, no math, TARGETS shows `<unknown>` forever. The task text told you ("part of the task is making the HPA functional"). `behavior` requires autoscaling/v2.
|
||||
**Grader checks:** the 70 + 240 fields AND cpu requests present on the deployment.
|
||||
|
||||
---
|
||||
|
||||
## Q11 — rollback (5%)
|
||||
|
||||
```bash
|
||||
kubectl -n ex-mercury rollout history deploy/release-app # rev1 1.25, rev2 1.26, rev3 1.27-bogus (stuck)
|
||||
kubectl -n ex-mercury rollout undo deploy/release-app # → back to rev2 content (1.26), recorded as rev4
|
||||
kubectl -n ex-mercury rollout status deploy/release-app
|
||||
kubectl -n ex-mercury get deploy release-app -o jsonpath='{.spec.template.spec.containers[0].image}' # nginx:1.26
|
||||
{ echo "image: nginx:1.26"; echo "rolled back to content of revision 2 (now revision 4)"; } > /tmp/exam/q11-rollback.txt
|
||||
```
|
||||
|
||||
**Why:** `undo` without `--to-revision` targets the last *fully deployed* revision — here 1.26. Note the revision numbering quirk: the rollback re-creates the old template as a NEW revision number; "revision you rolled back to" means the source revision (2).
|
||||
**Trap:** `--to-revision=1` overshoots to 1.25 — "previous working" is 1.26. `rollout history --revision=2` shows the template if unsure.
|
||||
**Grader checks:** image 1.26, deployment Available, file present.
|
||||
|
||||
---
|
||||
|
||||
## Q12 — taint + affinity combo (5%)
|
||||
|
||||
```bash
|
||||
kubectl -n ex-io create deploy edge-daemon --image=busybox --replicas=2 \
|
||||
--dry-run=client -o yaml -- sleep 3600 > /tmp/ed.yaml
|
||||
```
|
||||
Add to pod template spec:
|
||||
```yaml
|
||||
tolerations:
|
||||
- {key: tier, operator: Equal, value: critical, effect: NoSchedule}
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- {key: zone, operator: In, values: [east]}
|
||||
```
|
||||
|
||||
**Why both:** a toleration only *permits* landing on the tainted node — it doesn't attract; the scheduler could still place pods on worker2. Affinity only *restricts* to worker — but without the toleration the taint repels them and pods go Pending. Permission + constraint together = deterministic placement. (This is the one-sentence answer the drill-pack twin of this task demands.)
|
||||
**Grader checks:** both pods on drills-worker via `-o wide`.
|
||||
|
||||
---
|
||||
|
||||
## Q13 — WaitForFirstConsumer (5%)
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata: {name: vol-alpha}
|
||||
spec:
|
||||
capacity: {storage: 2Gi}
|
||||
accessModes: [ReadWriteOnce]
|
||||
storageClassName: local-manual
|
||||
hostPath: {path: /opt/vol-alpha}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata: {name: claim-alpha, namespace: ex-io}
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
storageClassName: local-manual
|
||||
resources: {requests: {storage: 1Gi}}
|
||||
```
|
||||
```bash
|
||||
echo "SC uses volumeBindingMode: WaitForFirstConsumer — binding deferred until a pod schedules, so scheduler can pick topology-compatible PV" > /tmp/exam/q13-why.txt
|
||||
kubectl -n ex-io run vol-user --image=nginx --dry-run=client -o yaml > /tmp/vu.yaml
|
||||
# add: volumes: [{name: d, persistentVolumeClaim: {claimName: claim-alpha}}]
|
||||
# volumeMounts: [{name: d, mountPath: /data}]
|
||||
kubectl apply -f /tmp/vu.yaml
|
||||
kubectl -n ex-io get pvc claim-alpha # Bound after pod schedules
|
||||
```
|
||||
|
||||
**Why:** WaitForFirstConsumer defers bind so node placement can inform PV choice — Pending-before-pod is *designed behavior*, not a fault. Also note 1Gi request binds to a 2Gi PV: binding requires PV ≥ request, not equality.
|
||||
**Grader checks:** the explanation file, Bound state, working mount.
|
||||
|
||||
---
|
||||
|
||||
## Q14 — default StorageClass (5%)
|
||||
|
||||
```bash
|
||||
kubectl patch sc local-manual -p \
|
||||
'{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
|
||||
kubectl apply -f - <<'EOF'
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata: {name: claim-default, namespace: ex-io}
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
resources: {requests: {storage: 100Mi}}
|
||||
EOF
|
||||
kubectl -n ex-io get pvc claim-default -o jsonpath='{.spec.storageClassName}' > /tmp/exam/q14-proof.txt # local-manual
|
||||
```
|
||||
|
||||
**Why:** the default-class annotation makes the admission plugin inject the class into class-less PVCs *at creation time*. Exact annotation string matters — it's on `kubernetes.io/docs/tasks/administer-cluster/change-default-storage-class/`.
|
||||
**Trap:** kind ships `standard` (local-path) as default — real exam variant often requires *demoting* the old default first (`is-default-class: "false"`), since two defaults make behavior version-dependent. Do both to be safe.
|
||||
**Grader checks:** annotation true, PVC auto-assigned the class.
|
||||
|
||||
---
|
||||
|
||||
## Q15 — Ingress generator (5%)
|
||||
|
||||
```bash
|
||||
kubectl -n ex-mars create ingress api-ingress \
|
||||
--class=nginx \
|
||||
--rule="api.exam.local/v1*=api-svc:80"
|
||||
kubectl -n ex-mars get ingress api-ingress -o jsonpath='{.spec.rules[0].http.paths[0].pathType}' # Prefix
|
||||
```
|
||||
|
||||
**Why:** the trailing `*` in the rule path is what emits `pathType: Prefix`; bare `/v1` → `Exact` → fails the explicit requirement. `--class` sets `ingressClassName`. Whole task is one generator line — hand-writing this YAML is the time-loss trap.
|
||||
**Grader checks:** host, path, pathType Prefix, class, backend svc:port.
|
||||
|
||||
---
|
||||
|
||||
## Q16 — crashing pod: capture then fix (6%)
|
||||
|
||||
```bash
|
||||
kubectl -n ex-rescue logs data-proc > /tmp/exam/q16-logs.txt # works after termination — logs persist
|
||||
kubectl -n ex-rescue delete pod data-proc $now
|
||||
kubectl -n ex-rescue run data-proc --image=busybox --restart=Never -- sleep 3600
|
||||
kubectl -n ex-rescue get pod data-proc # Running
|
||||
```
|
||||
|
||||
**Why:** ORDER is the trap — capture logs BEFORE delete; deleting first destroys the evidence and the grader's grep for the FATAL line fails. `kubectl logs` works on Failed pods (container logs persist until pod object removal). If a pod is crash-looping (restarting), `logs --previous` gets the prior attempt.
|
||||
**Grader checks:** FATAL line in the file + same-name pod Running.
|
||||
|
||||
---
|
||||
|
||||
## Q17 — broken static pod (5%)
|
||||
|
||||
```bash
|
||||
docker exec -it drills-worker bash
|
||||
cat /etc/kubernetes/manifests/edge-cache.yaml # image: redis:7-alpinee ← typo
|
||||
sed -i 's/alpinee/alpine/' /etc/kubernetes/manifests/edge-cache.yaml
|
||||
exit
|
||||
kubectl get pod edge-cache-drills-worker # Running (mirror pod = name + node suffix)
|
||||
```
|
||||
|
||||
**Why:** static pods are kubelet-local — the file in `staticPodPath` IS the source of truth; editing it makes kubelet recreate the pod (no apply, no API involvement). The mirror pod visible in kubectl is read-only — deleting it via kubectl just respawns it; fixes happen on the node.
|
||||
**Trap:** hunting for a deployment/controller that doesn't exist. "Configured on the node + invisible/broken in the API" should scream *static pod* — go read the manifests dir. Also: with ImagePullBackOff the mirror pod may actually be visible-but-broken; either way the fix path is identical.
|
||||
**Grader checks:** mirror pod Running in default ns.
|
||||
|
||||
---
|
||||
|
||||
## Meta-lessons across the paper
|
||||
1. **Every troubleshooting Q was diagnosable from `describe`/Events/logs in <60s** — the fix is trivial once the read is right. Budget reading time, not typing time.
|
||||
2. **Generators + patch covered 10 of 17 tasks** with zero hand-written YAML from scratch.
|
||||
3. **Traps cluster around order-of-operations** (logs before delete, demote old default, capture before fix) and **silent partial success** (one of two faults fixed, values lost on upgrade, OR instead of AND). The verification lap exists to catch exactly these.
|
||||
315
kubernetes/mock exams/cka-exam-pack-2-solutions.md
Normal file
315
kubernetes/mock exams/cka-exam-pack-2-solutions.md
Normal file
@@ -0,0 +1,315 @@
|
||||
# CKA Mock Exam Pack v2 — SOLUTIONS
|
||||
|
||||
> ⚠️ Post-attempt autopsy only. Fastest path → why → trap → grader focus.
|
||||
|
||||
## Q1 — dead scheduler (8%)
|
||||
```bash
|
||||
kubectl -n kube-system get pods | grep scheduler # missing or CrashLoop
|
||||
docker exec -it drills-control-plane bash
|
||||
crictl ps -a | grep sched # nothing healthy
|
||||
grep command -A3 /etc/kubernetes/manifests/kube-scheduler.yaml # kube-schedulerr ← typo
|
||||
sed -i 's|kube-schedulerr|kube-scheduler|' /etc/kubernetes/manifests/kube-scheduler.yaml
|
||||
exit
|
||||
kubectl -n kube-system get pods | grep scheduler # Running
|
||||
echo kube-scheduler > /tmp/exam2/q1.txt
|
||||
```
|
||||
**Why:** Pending + zero events = nothing is *making* scheduling decisions → scheduler. Static-pod manifests on the CP are the first read; kubelet auto-restarts on file change.
|
||||
**Trap/meta:** fix this FIRST — Q4, Q13, Q14 and every new pod depend on it. Triage was the real test.
|
||||
**Grader:** scheduler pod healthy, new pods schedule.
|
||||
|
||||
## Q2 — CoreDNS Corefile (7%)
|
||||
```bash
|
||||
kubectl -n kube-system logs deploy/coredns | tail # unknown directive "bogusplugin"
|
||||
kubectl -n kube-system edit cm coredns # restore a valid Corefile:
|
||||
```
|
||||
```
|
||||
.:53 {
|
||||
errors
|
||||
health { lameduck 5s }
|
||||
ready
|
||||
kubernetes cluster.local in-addr.arpa ip6.arpa {
|
||||
pods insecure
|
||||
fallthrough in-addr.arpa ip6.arpa
|
||||
ttl 30
|
||||
}
|
||||
prometheus :9153
|
||||
forward . /etc/resolv.conf { max_concurrent 1000 }
|
||||
cache 30
|
||||
loop
|
||||
reload
|
||||
loadbalance
|
||||
}
|
||||
```
|
||||
```bash
|
||||
kubectl -n kube-system rollout restart deploy coredns
|
||||
echo "invalid plugin directive in Corefile broke coredns" > /tmp/exam2/q2.txt
|
||||
```
|
||||
**Why:** crash-looping CoreDNS after a config change = read its logs; it names the bad directive. The sabotage also dropped the `kubernetes` plugin — without it cluster names never resolve even if pods run. The canonical Corefile is on `kubernetes.io/docs/tasks/administer-cluster/dns-custom-nameservers/`.
|
||||
**Trap:** restarting pods without fixing the CM; or fixing syntax but not restoring the `kubernetes` block.
|
||||
**Grader:** nslookup `kubernetes.default` succeeds.
|
||||
|
||||
## Q3 — broken kubeconfig (6%)
|
||||
```bash
|
||||
kubectl --kubeconfig /tmp/exam2/developer.kubeconfig get nodes # connection refused :PORT
|
||||
grep server /tmp/exam2/developer.kubeconfig
|
||||
grep server ~/.kube/config # compare — port off by one
|
||||
sed -i 's|:WRONGPORT|:RIGHTPORT|' /tmp/exam2/developer.kubeconfig
|
||||
echo "server URL had wrong apiserver port" > /tmp/exam2/q3.txt
|
||||
```
|
||||
**Why:** kubeconfig triage = three suspects: server URL (connection refused/timeout), CA data (x509 errors), client creds (401/403). Refused ⇒ URL. Diff against a working config instead of guessing.
|
||||
**Grader:** the command works using *that file*; admin config untouched.
|
||||
|
||||
## Q4 — PDB-blocked drain (5%)
|
||||
```bash
|
||||
kubectl drain drills-worker2 --ignore-daemonsets --delete-emptydir-data # evictions blocked by pinned-pdb
|
||||
kubectl -n ex2-neptune get pdb pinned-pdb # maxUnavailable: 0 — nothing may ever be evicted
|
||||
kubectl -n ex2-neptune patch pdb pinned-pdb --type merge -p '{"spec":{"maxUnavailable":1}}'
|
||||
kubectl -n ex2-neptune patch deploy pinned --type=json -p='[{"op":"remove","path":"/spec/template/spec/nodeSelector"}]'
|
||||
kubectl drain drills-worker2 --ignore-daemonsets --delete-emptydir-data # proceeds
|
||||
kubectl uncordon drills-worker2
|
||||
echo "PDB maxUnavailable:0 blocked eviction" > /tmp/exam2/q4.txt
|
||||
```
|
||||
**Why:** `maxUnavailable: 0` makes every eviction violate the budget — drain retries forever. `maxUnavailable: 1` (or `minAvailable: 1` on 2 replicas) is the minimal loosening that keeps ≥1 alive. Second wrinkle: the deploy is nodeSelector-pinned to the draining node — evicted pods can't reschedule elsewhere and would wedge availability; dropping the selector (or tolerating pending-until-uncordon, defensible if argued) completes it.
|
||||
**Trap:** deleting the PDB = not "minimal + still protective"; graders check the PDB still exists.
|
||||
|
||||
## Q5 — readiness probe (4%)
|
||||
```bash
|
||||
kubectl -n ex2-front describe pod -l app=shop-ui | grep -A3 Readiness # httpGet :8080 — nginx listens on 80
|
||||
kubectl -n ex2-front patch deploy shop-ui --type=json \
|
||||
-p='[{"op":"replace","path":"/spec/template/spec/containers/0/readinessProbe/httpGet/port","value":80}]'
|
||||
```
|
||||
**Why:** Running-but-not-Ready ⇒ probe. `describe` shows probe target and failure events; nginx serves :80.
|
||||
**Grader:** 2/2 READY.
|
||||
|
||||
## Q6 — etcd backup AND restore (8%)
|
||||
```bash
|
||||
docker exec -it drills-control-plane bash
|
||||
ETCDCTL_API=3 etcdctl snapshot save /root/etcd-v2.db \
|
||||
--endpoints=https://127.0.0.1:2379 \
|
||||
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
|
||||
--cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key
|
||||
exit
|
||||
kubectl create cm marker -n default --from-literal=state=after-backup
|
||||
docker exec -it drills-control-plane bash
|
||||
ETCDCTL_API=3 etcdutl snapshot restore /root/etcd-v2.db --data-dir /var/lib/etcd-restore # etcdctl also works
|
||||
sed -i 's|path: /var/lib/etcd$|path: /var/lib/etcd-restore|' /etc/kubernetes/manifests/etcd.yaml
|
||||
# kubelet notices manifest change, restarts etcd on the restored dir; apiserver reconnects (~30-60s)
|
||||
exit
|
||||
kubectl get cm marker -n default # NotFound — restore proven
|
||||
```
|
||||
**Why:** restore never touches the live data dir — unpack the snapshot to a NEW dir, repoint the static pod's hostPath. The marker CM created *after* the snapshot must vanish: that's the only honest proof a restore happened.
|
||||
**Trap:** editing `--data-dir` flag but not the volumes hostPath (or vice versa) — the hostPath mount is what matters since the container path stays `/var/lib/etcd`... check BOTH lines in etcd.yaml; safest is changing the hostPath only. Expect a scary minute of apiserver flapping — that's normal.
|
||||
**Grader:** marker gone, snapshot file present, cluster healthy.
|
||||
|
||||
## Q7 — broken chart (6%)
|
||||
```bash
|
||||
helm lint /tmp/exam2/shipper
|
||||
# fault 1: Chart.yaml apiVersion "v3" invalid → v2
|
||||
sed -i 's/^apiVersion: v3/apiVersion: v2/' /tmp/exam2/shipper/Chart.yaml
|
||||
helm template /tmp/exam2/shipper
|
||||
# fault 2: deployment.yaml — unclosed action {{ .Values.replicaCount }
|
||||
sed -i 's|{{ .Values.replicaCount }$|{{ .Values.replicaCount }}|' /tmp/exam2/shipper/templates/deployment.yaml
|
||||
helm install shipper /tmp/exam2/shipper -n ex2-batch --set replicaCount=2
|
||||
```
|
||||
**Why:** the debug ladder is `lint` (metadata/structure) → `template` (render/syntax) → `install --dry-run` (cluster validation). lint catches Chart.yaml; only template rendering exposes the brace fault — one tool doesn't see both, which is the lesson.
|
||||
**Grader:** release deployed, 2 replicas via CLI override.
|
||||
|
||||
## Q8 — ValidatingAdmissionPolicy (6%)
|
||||
```yaml
|
||||
apiVersion: admissionregistration.k8s.io/v1
|
||||
kind: ValidatingAdmissionPolicy
|
||||
metadata: {name: require-owner}
|
||||
spec:
|
||||
failurePolicy: Fail
|
||||
matchConstraints:
|
||||
resourceRules:
|
||||
- apiGroups: ["apps"]
|
||||
apiVersions: ["v1"]
|
||||
operations: ["CREATE"]
|
||||
resources: ["deployments"]
|
||||
validations:
|
||||
- expression: "has(object.metadata.labels) && 'owner' in object.metadata.labels"
|
||||
message: "deployment must carry an 'owner' label"
|
||||
---
|
||||
apiVersion: admissionregistration.k8s.io/v1
|
||||
kind: ValidatingAdmissionPolicyBinding
|
||||
metadata: {name: require-owner-binding}
|
||||
spec:
|
||||
policyName: require-owner
|
||||
validationActions: [Deny]
|
||||
matchResources:
|
||||
namespaceSelector:
|
||||
matchLabels: {env: guarded}
|
||||
```
|
||||
Prove:
|
||||
```bash
|
||||
kubectl -n ex2-guard create deploy bad --image=nginx # denied, message shown
|
||||
kubectl -n ex2-guard create deploy good --image=nginx --dry-run=client -o yaml \
|
||||
| kubectl label --local -f - owner=me -o yaml | kubectl apply -f - # accepted
|
||||
```
|
||||
**Why:** VAP = CEL expression evaluated in-apiserver, no webhook infra. Policy defines the rule; the *binding* scopes it (namespaceSelector) and sets the action — forgetting the binding = policy silently inert. `has()` guard first: CEL errors on absent maps otherwise.
|
||||
**Trap:** validation block in the exam paper creates-then-labels — that ORDER gets denied; the label must exist at CREATE. Spotting that is part of the task.
|
||||
**Grader:** deny with "owner" in message; labeled create passes.
|
||||
**Docs:** `kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/`.
|
||||
|
||||
## Q9 — kustomize from scratch (5%)
|
||||
`/tmp/exam2/kz/deploy.yaml`: plain nginx:1.27 deployment `board` (1 replica) with `envFrom: [{configMapRef: {name: board-cfg}}]`.
|
||||
`/tmp/exam2/kz/replicas.yaml`:
|
||||
```yaml
|
||||
- op: replace
|
||||
path: /spec/replicas
|
||||
value: 3
|
||||
```
|
||||
`/tmp/exam2/kz/kustomization.yaml`:
|
||||
```yaml
|
||||
namespace: ex2-batch
|
||||
resources: [deploy.yaml]
|
||||
configMapGenerator:
|
||||
- name: board-cfg
|
||||
literals: [MODE=exam]
|
||||
patches:
|
||||
- path: replicas.yaml
|
||||
target: {kind: Deployment, name: board}
|
||||
```
|
||||
```bash
|
||||
kubectl apply -k /tmp/exam2/kz
|
||||
```
|
||||
**Why:** configMapGenerator emits `board-cfg-<hash>` AND rewrites every reference to it — that's why the deployment references the plain name and kustomize wires the suffix. JSON6902 patch = op/path/value list with a target selector.
|
||||
**Trap:** creating the CM manually with the literal name — then the generated/hash mechanics the task demands never happen.
|
||||
**Grader:** replicas 3, hashed CM present, envFrom points at hashed name.
|
||||
|
||||
## Q10 — weighted split + ReferenceGrant (8%)
|
||||
```yaml
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata: {name: split-route, namespace: ex2-web}
|
||||
spec:
|
||||
parentRefs: [{name: shop-gate}]
|
||||
rules:
|
||||
- matches: [{path: {type: PathPrefix, value: /shop}}]
|
||||
backendRefs:
|
||||
- {name: web-v1, port: 5678, weight: 90}
|
||||
- name: web-v2
|
||||
namespace: ex2-canary
|
||||
port: 5678
|
||||
weight: 10
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: ReferenceGrant
|
||||
metadata: {name: allow-web-routes, namespace: ex2-canary} # lives in the TARGET ns
|
||||
spec:
|
||||
from:
|
||||
- {group: gateway.networking.k8s.io, kind: HTTPRoute, namespace: ex2-web}
|
||||
to:
|
||||
- {group: "", kind: Service}
|
||||
```
|
||||
**Why:** weights on sibling backendRefs in ONE rule = traffic split (proportions of summed weights). Cross-namespace backendRefs are denied by default — ReferenceGrant is consent, and it lives in the namespace being *referenced* (the target grants, the referrer can't self-authorize). That direction is the entire question.
|
||||
**Trap:** grant in ex2-web (wrong side); or two separate rules instead of two weighted backendRefs (that's not a split, first match wins).
|
||||
**Grader:** weights 90/10, namespace on v2 ref, grant in ex2-canary with correct from/to.
|
||||
|
||||
## Q11 — egress netpol + DNS (7%)
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata: {name: app-egress, namespace: ex2-data}
|
||||
spec:
|
||||
podSelector: {matchLabels: {role: app}}
|
||||
policyTypes: [Egress]
|
||||
egress:
|
||||
- to: [{podSelector: {matchLabels: {role: db}}}]
|
||||
ports: [{protocol: TCP, port: 80}]
|
||||
- ports:
|
||||
- {protocol: UDP, port: 53}
|
||||
- {protocol: TCP, port: 53}
|
||||
```
|
||||
**Why:** once `Egress` is a policyType, everything outbound not allowlisted dies — including DNS to kube-system. The bare-`ports` rule (no `to`) = "port 53 to anywhere", the standard DNS carve-out. Without it, even `curl $DBIP` by IP works but anything by *name* fails — the classic silent egress-policy footgun.
|
||||
**Trap:** forgetting DNS; or scoping the db rule with a namespaceSelector it doesn't need (same-ns podSelector suffices).
|
||||
**Grader:** app→db 200, app→rogue timeout.
|
||||
|
||||
## Q12 — NodePort (5%)
|
||||
```bash
|
||||
kubectl -n ex2-web expose deploy web-v1 --name=web-np --port=5678 --type=NodePort \
|
||||
--dry-run=client -o yaml > /tmp/np.yaml
|
||||
# add under ports[0]: nodePort: 30080 (expose can't set it)
|
||||
kubectl apply -f /tmp/np.yaml
|
||||
docker exec drills-worker curl -s -m 2 localhost:30080 # v1
|
||||
```
|
||||
**Why:** the generator can't pin nodePort — generate, add one field, apply. NodePort listens on every node regardless of pod placement (kube-proxy routes) — hence curl works from any node.
|
||||
**Grader:** fixed 30080, in-range (30000-32767), reachable.
|
||||
|
||||
## Q13 — spread + priority (6%)
|
||||
```yaml
|
||||
apiVersion: scheduling.k8s.io/v1
|
||||
kind: PriorityClass
|
||||
metadata: {name: exam-critical}
|
||||
value: 100000
|
||||
globalDefault: false
|
||||
```
|
||||
Deployment adds:
|
||||
```yaml
|
||||
priorityClassName: exam-critical
|
||||
topologySpreadConstraints:
|
||||
- maxSkew: 1
|
||||
topologyKey: kubernetes.io/hostname
|
||||
whenUnsatisfiable: DoNotSchedule
|
||||
labelSelector: {matchLabels: {app: spread-app}}
|
||||
```
|
||||
**Why:** PriorityClass is cluster-scoped, referenced by name in the pod spec. The spread constraint's own labelSelector is mandatory and must match the pods (self-selection) — its absence makes the constraint a no-op, the classic miss. CP taint keeps pods off the control-plane, so 4 replicas ⇒ 2+2 across workers with maxSkew 1.
|
||||
**Grader:** 2/2 split, both fields present.
|
||||
|
||||
## Q14 — DaemonSet incl. CP (5%)
|
||||
```bash
|
||||
kubectl -n ex2-batch create deploy node-agent --image=busybox --dry-run=client -o yaml -- sleep 3600 > /tmp/ds.yaml
|
||||
# edit: kind: DaemonSet, delete replicas+strategy, add toleration:
|
||||
# tolerations: [{key: node-role.kubernetes.io/control-plane, operator: Exists, effect: NoSchedule}]
|
||||
kubectl apply -f /tmp/ds.yaml
|
||||
```
|
||||
**Why:** no DS generator exists — mutate a deploy skeleton (delete `replicas`, `strategy`; change kind). DS schedules per-node automatically; only the CP taint stands between you and 3/3, hence the toleration.
|
||||
**Grader:** DESIRED=READY=3, one pod on drills-control-plane.
|
||||
|
||||
## Q15 — native sidecar (4%)
|
||||
Pod template:
|
||||
```yaml
|
||||
volumes: [{name: logs, emptyDir: {}}]
|
||||
initContainers:
|
||||
- name: shipper
|
||||
image: busybox
|
||||
restartPolicy: Always # ← makes it a sidecar
|
||||
command: ["sh","-c","touch /var/log/audit/audit.log; tail -f /var/log/audit/audit.log"]
|
||||
volumeMounts: [{name: logs, mountPath: /var/log/audit}]
|
||||
containers:
|
||||
- name: app
|
||||
image: busybox
|
||||
command: ["sh","-c","while true; do date >> /var/log/audit/audit.log; sleep 5; done"]
|
||||
volumeMounts: [{name: logs, mountPath: /var/log/audit}]
|
||||
```
|
||||
**Why:** `restartPolicy: Always` on an initContainer = native sidecar — starts before `app`, doesn't block pod completion, restarts independently. A second regular container is the *wrong* answer when the task says sidecar (post-2025 curriculum distinction).
|
||||
**Grader:** restartPolicy on the init container + heartbeats in `logs -c shipper`.
|
||||
|
||||
## Q16 — Released PV rescue (6%)
|
||||
```bash
|
||||
kubectl get pv keeper-pv -o yaml | grep -A5 claimRef # points at the deleted old-claim
|
||||
kubectl patch pv keeper-pv --type=json -p='[{"op":"remove","path":"/spec/claimRef"}]'
|
||||
kubectl get pv keeper-pv # Available
|
||||
# create new-claim (1Gi RWO class keeper) → binds
|
||||
echo claimRef > /tmp/exam2/q16.txt
|
||||
```
|
||||
**Why:** `Retain` + PVC deletion ⇒ `Released`, and the stale `claimRef` (with the dead claim's UID) blocks rebinding forever — by design, so an admin consciously reviews data before reuse. Removing claimRef is that conscious act; data and PV survive.
|
||||
**Trap:** deleting/recreating the PV — explicitly forbidden by the task.
|
||||
**Grader:** same PV object (creation timestamp unchanged) now Bound to new-claim.
|
||||
|
||||
## Q17 — accessModes mismatch (4%)
|
||||
```bash
|
||||
kubectl -n ex2-store describe pvc wide-claim # no PV matches: claim wants RWX, PV offers RWO
|
||||
# PVC spec is immutable in the relevant fields — recreate:
|
||||
kubectl -n ex2-store delete pvc wide-claim
|
||||
# recreate identical but accessModes: [ReadWriteOnce] → binds narrow-pv
|
||||
echo "PVC requested RWX, PV only offers RWO — accessModes must be satisfiable" > /tmp/exam2/q17.txt
|
||||
```
|
||||
**Why:** binding requires the PV to offer *every* mode the claim requests; RWX ⊄ {RWO} ⇒ eternal Pending. AccessModes on a PVC can't be edited in place — delete/recreate is the legitimate path (deleting a Pending claim is safe; nothing bound).
|
||||
**Grader:** Bound + the one-liner.
|
||||
|
||||
## Meta
|
||||
1. **Fault-ordering was the exam:** dead scheduler poisoned four other tasks. On the real thing, a cluster-level symptom noticed in your minute-1 skim gets fixed first regardless of its own weight.
|
||||
2. **Direction-of-consent idioms** (ReferenceGrant in the target ns; PV's claimRef as the binding brake) — Gateway/storage questions increasingly test *who authorizes whom*, not YAML recall.
|
||||
3. **Every "impossible" state had a one-field fix** (claimRef, accessModes, probe port, Corefile directive, one `r` in a binary name). Harder papers aren't more typing — they're more reading.
|
||||
237
kubernetes/mock exams/cka-exam-pack-2.md
Normal file
237
kubernetes/mock exams/cka-exam-pack-2.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# CKA Mock Exam Pack v2 — 120 min, harder cut
|
||||
|
||||
> 17 weighted tasks, 120 minutes, pass = 66. Range: kind `drills` (+ Cilium, Gateway API CRDs, metrics-server, helm).
|
||||
> **Difficulty delta vs v1:** control-plane components are broken and *tasks interact* — damage you don't fix early will block tasks you attempt later. Triage is part of the score.
|
||||
> Kind-isms: `ssh nodeX` → `docker exec -it drills-<node> bash`. One kubectl context.
|
||||
|
||||
## PROTOCOL
|
||||
1. Save MASTER SETUP to `/tmp/setup2.sh`, run blind: `bash /tmp/setup2.sh > /tmp/setup2.log 2>&1`. **Do not read it — spoilers.**
|
||||
2. `mkdir -p /tmp/exam2`. Hard 120:00 timer. No pauses, no Claude, no solutions file.
|
||||
3. Any order. Flag >8 min. Last 10 min = verification lap.
|
||||
4. After the timer: run VALIDATE blocks, fill the score sheet, then open the solutions doc for the autopsy.
|
||||
|
||||
## MASTER SETUP (paste blind)
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set +e
|
||||
for ns in ex2-neptune ex2-web ex2-canary ex2-data ex2-guard ex2-front ex2-batch ex2-side ex2-store; do kubectl create ns $ns; done
|
||||
|
||||
# workloads FIRST (scheduler dies later)
|
||||
kubectl -n ex2-front create deploy shop-ui --image=nginx --replicas=2
|
||||
kubectl -n ex2-front patch deploy shop-ui --type=json -p='[{"op":"add","path":"/spec/template/spec/containers/0/readinessProbe","value":{"httpGet":{"path":"/","port":8080},"periodSeconds":5}}]'
|
||||
kubectl -n ex2-data run db --image=nginx --labels=role=db --port=80
|
||||
kubectl -n ex2-data run app --image=nginx --labels=role=app --port=80
|
||||
kubectl -n ex2-data run rogue --image=nginx --labels=role=rogue --port=80
|
||||
kubectl -n ex2-web create deploy web-v1 --image=hashicorp/http-echo -- /http-echo -text=v1 -listen=:5678
|
||||
kubectl -n ex2-web expose deploy web-v1 --port=5678
|
||||
kubectl -n ex2-canary create deploy web-v2 --image=hashicorp/http-echo -- /http-echo -text=v2 -listen=:5678
|
||||
kubectl -n ex2-canary expose deploy web-v2 --port=5678
|
||||
kubectl apply -f - <<'EOF'
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: GatewayClass
|
||||
metadata: {name: exam2-class}
|
||||
spec: {controllerName: example.com/exam2}
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: Gateway
|
||||
metadata: {name: shop-gate, namespace: ex2-web}
|
||||
spec:
|
||||
gatewayClassName: exam2-class
|
||||
listeners: [{name: http, port: 80, protocol: HTTP}]
|
||||
EOF
|
||||
kubectl -n ex2-neptune create deploy pinned --image=nginx --replicas=2
|
||||
kubectl -n ex2-neptune patch deploy pinned --type=json -p='[{"op":"add","path":"/spec/template/spec/nodeSelector","value":{"kubernetes.io/hostname":"drills-worker2"}}]'
|
||||
kubectl apply -f - <<'EOF'
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata: {name: pinned-pdb, namespace: ex2-neptune}
|
||||
spec:
|
||||
maxUnavailable: 0
|
||||
selector: {matchLabels: {app: pinned}}
|
||||
EOF
|
||||
kubectl label ns ex2-guard env=guarded
|
||||
# storage scenarios
|
||||
kubectl apply -f - <<'EOF'
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata: {name: keeper-pv}
|
||||
spec:
|
||||
capacity: {storage: 1Gi}
|
||||
accessModes: [ReadWriteOnce]
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: keeper
|
||||
hostPath: {path: /opt/keeper}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata: {name: old-claim, namespace: ex2-store}
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
storageClassName: keeper
|
||||
resources: {requests: {storage: 1Gi}}
|
||||
EOF
|
||||
sleep 5; kubectl -n ex2-store delete pvc old-claim # -> PV Released
|
||||
kubectl apply -f - <<'EOF'
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata: {name: narrow-pv}
|
||||
spec:
|
||||
capacity: {storage: 1Gi}
|
||||
accessModes: [ReadWriteOnce]
|
||||
storageClassName: narrow
|
||||
hostPath: {path: /opt/narrow}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata: {name: wide-claim, namespace: ex2-store}
|
||||
spec:
|
||||
accessModes: [ReadWriteMany]
|
||||
storageClassName: narrow
|
||||
resources: {requests: {storage: 1Gi}}
|
||||
EOF
|
||||
# broken helm chart
|
||||
mkdir -p /tmp/exam2 && cd /tmp/exam2 && helm create shipper >/dev/null 2>&1
|
||||
sed -i 's/^apiVersion: v2/apiVersion: v3/' /tmp/exam2/shipper/Chart.yaml
|
||||
sed -i 's/{{ .Values.replicaCount }}/{{ .Values.replicaCount }/' /tmp/exam2/shipper/templates/deployment.yaml
|
||||
# broken kubeconfig for developer
|
||||
kubectl config view --raw --minify > /tmp/exam2/developer.kubeconfig
|
||||
SRV=$(grep server /tmp/exam2/developer.kubeconfig | awk '{print $2}')
|
||||
PORT=${SRV##*:}; NEW=$((PORT+1))
|
||||
sed -i "s|$SRV|${SRV%:*}:$NEW|" /tmp/exam2/developer.kubeconfig
|
||||
kubectl -n ex2-front wait --for=condition=Available deploy --all --timeout=90s
|
||||
kubectl -n ex2-data wait --for=condition=Ready pod --all --timeout=90s
|
||||
# BREAK coredns
|
||||
kubectl -n kube-system get cm coredns -o yaml > /tmp/exam2/.coredns-backup.yaml
|
||||
kubectl -n kube-system patch cm coredns --type merge -p '{"data":{"Corefile":".:53 {\n errors\n forward . /etc/resolv.conf\n bogusplugin\n}\n"}}'
|
||||
kubectl -n kube-system rollout restart deploy coredns
|
||||
# BREAK scheduler (last)
|
||||
docker exec drills-control-plane sed -i 's|kube-scheduler|kube-schedulerr|' /etc/kubernetes/manifests/kube-scheduler.yaml
|
||||
echo "SETUP2 DONE"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## THE EXAM
|
||||
|
||||
### Q1 — 8% — Troubleshooting
|
||||
New Pods across the cluster stay `Pending` with no scheduling events. A control-plane component is at fault. Find it, fix it on the node, prove new pods schedule again. Component name → `/tmp/exam2/q1.txt`.
|
||||
|
||||
### Q2 — 7% — Troubleshooting
|
||||
Cluster DNS is down (CoreDNS crash-looping). Diagnose and repair so that a busybox:1.28 pod can `nslookup kubernetes.default`. Root cause, one line → `/tmp/exam2/q2.txt`.
|
||||
|
||||
### Q3 — 6% — Troubleshooting
|
||||
The kubeconfig at `/tmp/exam2/developer.kubeconfig` is broken — `kubectl --kubeconfig /tmp/exam2/developer.kubeconfig get nodes` fails. Fix **the file** (not your admin config) so the command succeeds. Fault, one line → `/tmp/exam2/q3.txt`.
|
||||
|
||||
### Q4 — 5% — Troubleshooting
|
||||
Drain node `drills-worker2` (ignore DaemonSets). Something will block it. Resolve the blocker with the **minimal** change that still keeps ≥1 replica of the affected app available at all times, complete the drain, then uncordon. Blocker → `/tmp/exam2/q4.txt`.
|
||||
|
||||
### Q5 — 4% — Troubleshooting
|
||||
Deployment `shop-ui` in `ex2-front` shows 0/2 READY though containers run. Find why and fix the deployment (the container itself is fine).
|
||||
|
||||
### Q6 — 8% — Cluster Architecture
|
||||
Take an etcd snapshot to `/root/etcd-v2.db` on the control-plane node. Then create ConfigMap `marker` (`kubectl create cm marker -n default --from-literal=state=after-backup`), and **restore the snapshot** so that `marker` no longer exists. Prove it.
|
||||
|
||||
### Q7 — 6% — Cluster Architecture
|
||||
The Helm chart at `/tmp/exam2/shipper` fails to install. Find and fix **all** faults (start with `helm lint`), then install it as release `shipper` in namespace `ex2-batch` with 2 replicas set via CLI.
|
||||
|
||||
### Q8 — 6% — Cluster Architecture
|
||||
Namespace `ex2-guard` is labeled `env=guarded`. Create a ValidatingAdmissionPolicy `require-owner` + binding: every Deployment created in namespaces labeled `env=guarded` must carry a label `owner` (any value); violations are **denied** with a message mentioning "owner". Prove: one denied create, one accepted.
|
||||
|
||||
### Q9 — 5% — Cluster Architecture
|
||||
Under `/tmp/exam2/kz`, build a kustomization (no base reuse — from scratch): deploys `nginx:1.27` deployment `board`, namespace `ex2-batch`, replicas patched to 3 via a **JSON6902 patch**, plus a generated ConfigMap `board-cfg` with `MODE=exam`. Apply with `-k`. The deployment must reference the generated ConfigMap via `envFrom` (hash suffix handled by kustomize).
|
||||
|
||||
### Q10 — 8% — Services & Networking
|
||||
Create HTTPRoute `split-route` in `ex2-web` on Gateway `shop-gate`: path prefix `/shop`, traffic split **90%** to `web-v1:5678` (same ns) and **10%** to `web-v2:5678` — which lives in namespace `ex2-canary`. Make the cross-namespace backend reference legal. Spec-level correctness counts.
|
||||
|
||||
### Q11 — 7% — Services & Networking
|
||||
In `ex2-data`: Pods `role=app` may send egress **only** to Pods `role=db` on TCP 80, plus DNS (TCP+UDP 53) anywhere. Everything else outbound denied. Policy name `app-egress`. Prove: app→db works, app→rogue times out.
|
||||
|
||||
### Q12 — 5% — Services & Networking
|
||||
Expose deployment `web-v1` in `ex2-web` via a NodePort service `web-np` on port 5678, nodePort **30080**. Prove reachability with curl **from a node** (docker exec).
|
||||
|
||||
### Q13 — 6% — Workloads & Scheduling
|
||||
Create PriorityClass `exam-critical` (value 100000, not default). In `ex2-batch` create deployment `spread-app` (nginx, 4 replicas, priorityClassName `exam-critical`) with a topologySpreadConstraint: maxSkew 1 over `kubernetes.io/hostname`, `DoNotSchedule`. End state: 2 pods per worker.
|
||||
|
||||
### Q14 — 5% — Workloads & Scheduling
|
||||
Create DaemonSet `node-agent` in `ex2-batch` (busybox, `sleep 3600`) that runs on **all three nodes including the control-plane**. Prove 3/3.
|
||||
|
||||
### Q15 — 4% — Workloads & Scheduling
|
||||
In `ex2-side`: deployment `audit-app` (1 replica): main container `app` (busybox) writes the date to `/var/log/audit/audit.log` every 5s; **native sidecar** `shipper` (busybox) tails it. Shared emptyDir. Prove heartbeats via `kubectl logs ... -c shipper`.
|
||||
|
||||
### Q16 — 6% — Storage
|
||||
PV `keeper-pv` is `Released` and must be reused **without deleting the PV or its data**. Make it bindable again and bind a new PVC `new-claim` (1Gi, RWO, class `keeper`) in `ex2-store`. Both must reach `Bound`. What field did you touch → `/tmp/exam2/q16.txt`.
|
||||
|
||||
### Q17 — 4% — Storage
|
||||
PVC `wide-claim` in `ex2-store` is `Pending` and will never bind. Diagnose; fix by changing **the claim** so it binds to PV `narrow-pv`. Root cause → `/tmp/exam2/q17.txt`.
|
||||
|
||||
---
|
||||
|
||||
## VALIDATE (after the timer)
|
||||
|
||||
```bash
|
||||
# Q1
|
||||
kubectl -n default run probe --image=nginx --restart=Never && sleep 5 && kubectl get pod probe # Running
|
||||
cat /tmp/exam2/q1.txt # kube-scheduler
|
||||
# Q2
|
||||
kubectl run dnstest --image=busybox:1.28 --restart=Never --rm -it -- nslookup kubernetes.default # resolves
|
||||
# Q3
|
||||
kubectl --kubeconfig /tmp/exam2/developer.kubeconfig get nodes # 3 nodes
|
||||
# Q4
|
||||
kubectl get node drills-worker2 # Ready, SchedulingDisabled absent
|
||||
kubectl -n ex2-neptune get pdb pinned-pdb -o jsonpath='{.spec}' # loosened but still protective
|
||||
kubectl -n ex2-neptune get deploy pinned # 2/2 (back after uncordon or rescheduled)
|
||||
# Q5
|
||||
kubectl -n ex2-front get deploy shop-ui # 2/2
|
||||
# Q6
|
||||
kubectl get cm marker -n default # NotFound (proof of restore)
|
||||
docker exec drills-control-plane ls -la /root/etcd-v2.db # exists
|
||||
# Q7
|
||||
helm list -n ex2-batch | grep shipper # deployed
|
||||
kubectl -n ex2-batch get deploy -l app.kubernetes.io/name=shipper -o jsonpath='{.items[0].spec.replicas}' # 2
|
||||
# Q8
|
||||
kubectl -n ex2-guard create deploy bad --image=nginx 2>&1 | grep -i owner # denied, message mentions owner
|
||||
kubectl -n ex2-guard create deploy good --image=nginx && kubectl -n ex2-guard label deploy good owner=me # wrong order — see solutions
|
||||
kubectl -n ex2-guard delete deploy good 2>/dev/null
|
||||
# Q9
|
||||
kubectl -n ex2-batch get deploy board -o jsonpath='{.spec.replicas}' # 3
|
||||
kubectl -n ex2-batch get cm | grep board-cfg # board-cfg-<hash>
|
||||
# Q10
|
||||
kubectl -n ex2-web get httproute split-route -o yaml # weights 90/10, ns on the v2 backendRef
|
||||
kubectl -n ex2-canary get referencegrant -o yaml # allows HTTPRoute/ex2-web -> Service
|
||||
# Q11
|
||||
DBIP=$(kubectl -n ex2-data get pod db -o jsonpath='{.status.podIP}')
|
||||
RGIP=$(kubectl -n ex2-data get pod rogue -o jsonpath='{.status.podIP}')
|
||||
kubectl -n ex2-data exec app -- curl -s -m 2 $DBIP >/dev/null && echo DB-OK
|
||||
kubectl -n ex2-data exec app -- curl -s -m 2 $RGIP; echo EXIT=$? # EXIT=28
|
||||
# Q12
|
||||
docker exec drills-worker curl -s -m 2 localhost:30080 # v1
|
||||
# Q13
|
||||
kubectl -n ex2-batch get pods -l app=spread-app -o wide # 2 + 2 across workers
|
||||
# Q14
|
||||
kubectl -n ex2-batch get ds node-agent # DESIRED 3, READY 3
|
||||
# Q15
|
||||
kubectl -n ex2-side logs deploy/audit-app -c shipper | tail -3 # dated lines
|
||||
kubectl -n ex2-side get pod -o jsonpath='{.items[0].spec.initContainers[0].restartPolicy}' # Always
|
||||
# Q16
|
||||
kubectl get pv keeper-pv # Bound to ex2-store/new-claim
|
||||
cat /tmp/exam2/q16.txt # claimRef
|
||||
# Q17
|
||||
kubectl -n ex2-store get pvc wide-claim # Bound
|
||||
cat /tmp/exam2/q17.txt # accessModes mismatch
|
||||
```
|
||||
|
||||
## SCORE SHEET
|
||||
| Q | W | Dom | Pass | Time | | Q | W | Dom | Pass | Time |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| 1 | 8 | TS | | | | 10 | 8 | SN | | |
|
||||
| 2 | 7 | TS | | | | 11 | 7 | SN | | |
|
||||
| 3 | 6 | TS | | | | 12 | 5 | SN | | |
|
||||
| 4 | 5 | TS | | | | 13 | 6 | WS | | |
|
||||
| 5 | 4 | TS | | | | 14 | 5 | WS | | |
|
||||
| 6 | 8 | CA | | | | 15 | 4 | WS | | |
|
||||
| 7 | 6 | CA | | | | 16 | 6 | ST | | |
|
||||
| 8 | 6 | CA | | | | 17 | 4 | ST | | |
|
||||
| 9 | 5 | CA | | | | | | | | |
|
||||
|
||||
**Pass: 66.** Rebuild the range between attempts (`kind delete cluster --name drills` + range-up) — v2's damage is deep enough that RESET scripts lie.
|
||||
130
kubernetes/mock exams/cka-exams-env-setup.md
Normal file
130
kubernetes/mock exams/cka-exams-env-setup.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# CKA Range Bring-Up — kind + Cilium (matches Mock Exam Pack v1)
|
||||
|
||||
> Produces cluster `drills` on Kubernetes **v1.35.0** (= exam version) with nodes named exactly as the drill/mock packs expect: `drills-control-plane`, `drills-worker`, `drills-worker2`.
|
||||
> CNI: **Cilium 1.19.x** — real NetworkPolicy enforcement (kindnet has none; netpol drills would silently pass on it).
|
||||
> Versions verified 2026-07-29. Re-check pins if you're reading this much later: kind releases page, cilium.io stable docs.
|
||||
|
||||
---
|
||||
|
||||
## 0. Host prerequisites
|
||||
|
||||
- Docker (or compatible), ≥ 8GB RAM free for the 3 nodes
|
||||
- **cgroup v2 on the host** — mandatory: k8s 1.35 node images dropped cgroup v1. Check: `stat -fc %T /sys/fs/cgroup` → must print `cgroup2fs`. Any current distro qualifies.
|
||||
- Tools: `kind` ≥ v0.31.0 (ships the v1.35.0 default image), `kubectl`, `helm`
|
||||
```bash
|
||||
kind version # v0.31.x
|
||||
helm version # v3.x
|
||||
```
|
||||
|
||||
## 1. Cluster config — `kind-drills.yaml`
|
||||
|
||||
```yaml
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
name: drills # ⇒ node names drills-control-plane / drills-worker / drills-worker2
|
||||
networking:
|
||||
disableDefaultCNI: true # kindnet out, Cilium in
|
||||
nodes:
|
||||
- role: control-plane
|
||||
image: kindest/node:v1.35.0 # pin explicitly = exam version, survives kind upgrades
|
||||
- role: worker
|
||||
image: kindest/node:v1.35.0
|
||||
- role: worker
|
||||
image: kindest/node:v1.35.0
|
||||
```
|
||||
|
||||
Note: kube-proxy stays (default). Do NOT enable Cilium's kube-proxy replacement — the exam cluster runs kube-proxy, and iptables-visible Services are part of the troubleshooting surface you're training.
|
||||
|
||||
## 2. Bring-up — `range-up.sh`
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
CILIUM_VER=1.19.6
|
||||
|
||||
kind create cluster --config kind-drills.yaml
|
||||
# nodes will sit NotReady until CNI lands — expected
|
||||
|
||||
# --- Cilium (per cilium.io kind guide) ---
|
||||
docker pull quay.io/cilium/cilium:v${CILIUM_VER}
|
||||
kind load docker-image quay.io/cilium/cilium:v${CILIUM_VER} --name drills # skip registry pulls on each node
|
||||
|
||||
helm repo add cilium https://helm.cilium.io/ 2>/dev/null; helm repo update
|
||||
helm install cilium cilium/cilium --version ${CILIUM_VER} \
|
||||
--namespace kube-system \
|
||||
--set image.pullPolicy=IfNotPresent \
|
||||
--set ipam.mode=kubernetes
|
||||
|
||||
kubectl -n kube-system rollout status ds/cilium --timeout=180s
|
||||
kubectl wait --for=condition=Ready node --all --timeout=180s
|
||||
|
||||
# --- Gateway API CRDs (standard channel) ---
|
||||
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml
|
||||
|
||||
# --- metrics-server (for HPA TARGETS + kubectl top) ---
|
||||
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
|
||||
kubectl -n kube-system patch deploy metrics-server --type=json \
|
||||
-p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'
|
||||
# ^ mandatory on kind: kubelet serves self-signed certs; without this metrics-server never goes Ready
|
||||
|
||||
# --- helm repo used by drill K4 / mock Q8 ---
|
||||
helm repo add bitnami https://charts.bitnami.com/bitnami 2>/dev/null; helm repo update
|
||||
|
||||
echo "RANGE UP"
|
||||
```
|
||||
|
||||
## 3. Smoke test — run before ANY drill session
|
||||
|
||||
```bash
|
||||
kubectl get nodes -o wide
|
||||
# drills-control-plane / drills-worker / drills-worker2 — all Ready, VERSION v1.35.0
|
||||
|
||||
kubectl get pods -A | grep -v Running | grep -v Completed # empty
|
||||
kubectl api-resources | grep -i httproute # gateway.networking.k8s.io present
|
||||
kubectl top nodes # numbers (may need ~60s after install)
|
||||
kubectl -n kube-system exec ds/cilium -- cilium status --brief # OK
|
||||
|
||||
# NetworkPolicy ENFORCEMENT check — the one that matters (kindnet would pass traffic anyway):
|
||||
kubectl create ns smoke
|
||||
kubectl -n smoke run a --image=nginx --labels=app=a --port=80
|
||||
kubectl -n smoke run b --image=busybox -- sleep 600
|
||||
kubectl -n smoke wait --for=condition=Ready pod --all --timeout=60s
|
||||
kubectl -n smoke apply -f - <<'EOF'
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata: {name: deny-all}
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes: [Ingress]
|
||||
EOF
|
||||
AIP=$(kubectl -n smoke get pod a -o jsonpath='{.status.podIP}')
|
||||
kubectl -n smoke exec b -- wget -qO- -T 2 $AIP && echo "FAIL: netpol NOT enforced" || echo "OK: netpol enforced"
|
||||
kubectl delete ns smoke --wait=false
|
||||
```
|
||||
|
||||
If that last line prints FAIL, the CNI install went sideways — do not run K1/Q3 until fixed (`kubectl -n kube-system logs ds/cilium | tail`).
|
||||
|
||||
## 4. Static pod path sanity (mock Q17 depends on it)
|
||||
|
||||
```bash
|
||||
docker exec drills-worker grep staticPodPath /var/lib/kubelet/config.yaml
|
||||
# expect: staticPodPath: /etc/kubernetes/manifests
|
||||
```
|
||||
|
||||
## 5. Teardown / rebuild
|
||||
|
||||
```bash
|
||||
kind delete cluster --name drills # full nuke, ~10s
|
||||
# rebuild = run range-up.sh again, ~3 min total
|
||||
```
|
||||
|
||||
Rebuild is cheap — prefer a fresh cluster over archaeologically cleaning a broken one between mock attempts. The mock pack's RESET script is for same-day reruns; a new day gets a new cluster.
|
||||
|
||||
## 6. Known deltas vs the real exam (accept, don't fight)
|
||||
|
||||
| Real exam | This range |
|
||||
|---|---|
|
||||
| `ssh nodeX` | `docker exec -it drills-<node> bash` |
|
||||
| multiple kubectl contexts | one context; discipline = read each task's target ns/node |
|
||||
| kubeadm package upgrades (apt) | not possible — node image is baked; upgrade tasks stay on KodeKloud/killer |
|
||||
| several CNIs possible | Cilium everywhere (netpol semantics are standard, so drills transfer) |
|
||||
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 | | | | | |
|
||||
231
kubernetes/mock exams/exam-1.md
Normal file
231
kubernetes/mock exams/exam-1.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# Task 1
|
||||
|
||||
```bash
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
labels:
|
||||
run: mc-pod
|
||||
name: mc-pod
|
||||
namespace: mc-namespace
|
||||
spec:
|
||||
containers:
|
||||
- image: nginx:1-alpine
|
||||
name: mc-pod-1
|
||||
env:
|
||||
- name: NODE_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
- image: busybox:1
|
||||
name: mc-pod-2
|
||||
command: ["sh", "-c"]
|
||||
args: ["while true; do date >> /var/log/shared/date.log; sleep 1; done"]
|
||||
volumeMounts:
|
||||
- mountPath: /var/log/shared
|
||||
name: shared-log
|
||||
- image: busybox:1
|
||||
name: mc-pod-3
|
||||
command: ["sh", "-c"]
|
||||
args: ["tail -f /var/log/shared/date.log"]
|
||||
volumeMounts:
|
||||
- mountPath: /var/log/shared
|
||||
name: shared-log
|
||||
volumes:
|
||||
- name: shared-log
|
||||
emptyDir:
|
||||
sizeLimit: 500Mi
|
||||
|
||||
|
||||
echo "verticalpodautoscalercheckpoints" >> /root/vpa-crds.txt
|
||||
echo "verticalpodautoscalers" >> /root/vpa-crds.txt
|
||||
|
||||
|
||||
kubectl expose pod messaging --port=6379 --name=messaging-service
|
||||
|
||||
kubectl create deployment hr-web-app --image=kodekloud/webapp-color --replicas=2
|
||||
|
||||
cat webapp-hpa.yaml
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: webapp-hpa
|
||||
namespace: default
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: kkapp-deploy
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
|
||||
|
||||
|
||||
cat pv.yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: pv-analytics
|
||||
spec:
|
||||
hostPath:
|
||||
path: /pv/data-analytics
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
capacity:
|
||||
storage: 100Mi
|
||||
|
||||
cat orange.yaml
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
creationTimestamp: "2026-07-21T19:20:48Z"
|
||||
generation: 1
|
||||
name: orange
|
||||
namespace: default
|
||||
resourceVersion: "4629"
|
||||
uid: 3a8c1925-7db8-4518-9489-e6ae2e814ac5
|
||||
spec:
|
||||
containers:
|
||||
- command:
|
||||
- sh
|
||||
- -c
|
||||
- echo The app is running! && sleep 3600
|
||||
image: busybox:1.28
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: orange-container
|
||||
resources: {}
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
volumeMounts:
|
||||
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
|
||||
name: kube-api-access-jhc2d
|
||||
readOnly: true
|
||||
dnsPolicy: ClusterFirst
|
||||
enableServiceLinks: true
|
||||
initContainers:
|
||||
- command:
|
||||
- sh
|
||||
- -c
|
||||
- sleep 2;
|
||||
image: busybox
|
||||
imagePullPolicy: Always
|
||||
name: init-myservice
|
||||
resources: {}
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
volumeMounts:
|
||||
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
|
||||
name: kube-api-access-jhc2d
|
||||
readOnly: true
|
||||
nodeName: controlplane
|
||||
preemptionPolicy: PreemptLowerPriority
|
||||
priority: 0
|
||||
restartPolicy: Always
|
||||
schedulerName: default-scheduler
|
||||
securityContext: {}
|
||||
serviceAccount: default
|
||||
serviceAccountName: default
|
||||
terminationGracePeriodSeconds: 30
|
||||
tolerations:
|
||||
- effect: NoExecute
|
||||
key: node.kubernetes.io/not-ready
|
||||
operator: Exists
|
||||
tolerationSeconds: 300
|
||||
- effect: NoExecute
|
||||
key: node.kubernetes.io/unreachable
|
||||
operator: Exists
|
||||
tolerationSeconds: 300
|
||||
volumes:
|
||||
- name: kube-api-access-jhc2d
|
||||
projected:
|
||||
defaultMode: 420
|
||||
sources:
|
||||
- serviceAccountToken:
|
||||
expirationSeconds: 3607
|
||||
path: token
|
||||
- configMap:
|
||||
items:
|
||||
- key: ca.crt
|
||||
path: ca.crt
|
||||
name: kube-root-ca.crt
|
||||
- downwardAPI:
|
||||
items:
|
||||
- fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: metadata.namespace
|
||||
path: namespace
|
||||
|
||||
kubectl expose deployment hr-web-app --port=8080 --type=NodePort --name=hr-web-app-service
|
||||
|
||||
-----------
|
||||
|
||||
cat /root/webapp-hpa.yaml
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: webapp-hpa
|
||||
spec:
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- resource:
|
||||
name: cpu
|
||||
target:
|
||||
averageUtilization: 50
|
||||
type: Utilization
|
||||
type: Resource
|
||||
minReplicas: 1
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: kkapp-deploy
|
||||
status:
|
||||
currentMetrics: null
|
||||
desiredReplicas: 0
|
||||
|
||||
----------------------------
|
||||
|
||||
# vpa
|
||||
# ----------------
|
||||
|
||||
apiVersion: autoscaling.k8s.io/v1
|
||||
kind: VerticalPodAutoscaler
|
||||
metadata:
|
||||
name: analytics-vpa
|
||||
namespace: default
|
||||
spec:
|
||||
targetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: analytics-deployment
|
||||
updatePolicy:
|
||||
updateMode: "Recreate"
|
||||
resourcePolicy:
|
||||
containerPolicies:
|
||||
- containerName: "*"
|
||||
controlledResources: ["cpu", "memory"]
|
||||
|
||||
# -----------------
|
||||
# gateway
|
||||
# -------
|
||||
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: web-gateway
|
||||
namespace: nginx-gateway
|
||||
spec:
|
||||
gatewayClassName: nginx
|
||||
listeners:
|
||||
- name: http
|
||||
protocol: HTTP
|
||||
port: 80
|
||||
allowedRoutes:
|
||||
namespaces:
|
||||
from: Same
|
||||
|
||||
# --------
|
||||
helm upgrade -n kk-ns kk-mock1 kk-mock1/podinfo --version 6.11.2
|
||||
|
||||
# --------
|
||||
|
||||
```
|
||||
114
kubernetes/mock exams/exam-2.md
Normal file
114
kubernetes/mock exams/exam-2.md
Normal file
@@ -0,0 +1,114 @@
|
||||
```bash
|
||||
|
||||
# storageclass
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
name: local-sc
|
||||
annotations:
|
||||
storageclass.kubernetes.io/is-default-class: "true"
|
||||
provisioner: kubernetes.io/no-provisioner
|
||||
reclaimPolicy: Retain # default value is Delete
|
||||
volumeBindingMode: WaitForFirstConsumer
|
||||
|
||||
# -------------
|
||||
|
||||
# multipod deployment
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: logging-deployment
|
||||
name: logging-deployment
|
||||
namespace: logging-ns
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: logging-deployment
|
||||
strategy: {}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: logging-deployment
|
||||
spec:
|
||||
containers:
|
||||
- command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
mkdir -p /var/log/app
|
||||
while true; do
|
||||
echo "Log entry" >> /var/log/app/app.log
|
||||
sleep 5
|
||||
done
|
||||
image: busybox
|
||||
name: busybox
|
||||
volumeMounts:
|
||||
- name: log-vol
|
||||
mountPath: /var/log/app
|
||||
- name: log-agent
|
||||
image: busybox
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
touch /var/log/app/app.log
|
||||
tail -f /var/log/app/app.log
|
||||
volumeMounts:
|
||||
- name: log-vol
|
||||
mountPath: /var/log/app
|
||||
volumes:
|
||||
- name: log-vol
|
||||
emptyDir: {}
|
||||
|
||||
# -----------
|
||||
|
||||
kubectl create ingress webapp-ingress -n ingress-ns --rule="kodekloud-ingress.app/*=webapp-svc:80" --class=nginx -o yaml --dry-run > ingress.yaml
|
||||
|
||||
# --------
|
||||
|
||||
# create deployment, upgrade image
|
||||
kubectl create deployment nginx-deploy --image=nginx:1.16 --replicas=1
|
||||
kubectl set image deployment/nginx-deploy nginx=nginx:1.17
|
||||
|
||||
# --------
|
||||
|
||||
cat csr.yaml
|
||||
apiVersion: certificates.k8s.io/v1
|
||||
kind: CertificateSigningRequest
|
||||
metadata:
|
||||
name: john-developer # example
|
||||
spec:
|
||||
# This is an encoded CSR. Change this to the base64-encoded contents of myuser.csr
|
||||
request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ1ZEQ0NBVHdDQVFBd0R6RU5NQXNHQTFVRUF3d0VhbTlvYmpDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRApnZ0VQQURDQ0FRb0NnZ0VCQU5BTEhVNlMxSVE1WWU4QkE1SW0vZlJlVlhaQ0QrUTUxT0NocmtWdXZ2Y0RFcERRCjZvQ2hjRXJoOUVYWlEwL0g4eEVOSkZoTzY1cyszLzVqRmRWZXRWY2RrZk1UVWJJdUtvS092R3ZWSlU4dWowWnQKUy9KRjRGQkQvOXdndXk1azYvY3hFSlF1cHMzLzBwTCtqUi9rK01SM0ZscWZCZG1WV1dVMmxmYm5TWElsQnFLMwova3RMcUhadG9oT2ZVN0hYR2gwUEY5YThCaXpmK0REdE8ya3Z1VFhMMHBJV3FyVkx6QUhPZVdId1phVXpmUmUvCnR1UjFncHF2a2dTNFJPTWdHeExucHZZcXgyV0cxbDNGaTBzTDVxM1F0dkhzelMrZjQwcWVzd2pnU0JQcTM1amQKR1hIakFJdnY4MFliSWxub0FTd0oyUzR0Z3NvNG4yMitZc2FkTXpVQ0F3RUFBYUFBTUEwR0NTcUdTSWIzRFFFQgpDd1VBQTRJQkFRQ01reFAxWElaWUhSUHNuVHdlRGUzTDIzeWhJLzVlOWltbzZrTWd0MVhxRXFNeVJvTUc4dStKCmlPNmFHVFpMSE5QbFVKR3pqSWpGR2RsUUtDMVpiOEUyUVpsOFhjNjA5SzlTOFlFVjg2Y0tNL2xxR1Q3OFp1blEKb1ljRWpwRUZYUHMwQkZMQlYwRnRVeDNVd2JWYmltMGErOEtWSjhFNllnSjcrb2JOaURNc0NpR21hK1J0K0hmRwpHcE1vaEdhQXRoSFhuSEtYbUk1VjNBbmtKM0c3am1rdThWY1ZBYkl3NWs2L1RUQjBZUnQxUXdIZGxMUGU1ZVc4ClBCYnB1bEpaMWxocWhxV01lblVIOU9UTVI4b0ZiazFzaVpSN2VKeU5GMWtWYlRkQWVLT2FsMVk3TlcxamY1Y0gKREhwcFBZRW84ZlJ1UE5GTXFCNTlxQkM4bEF5MGd0MlcKLS0tLS1FTkQgQ0VSVElGSUNBVEUgUkVRVUVTVC0tLS0tCg==
|
||||
signerName: kubernetes.io/kube-apiserver-client
|
||||
expirationSeconds: 86400 # one day
|
||||
usages:
|
||||
- client auth
|
||||
|
||||
kubectl certificate approve john
|
||||
kubectl certificate approve john-developer
|
||||
|
||||
kubectl create role developer --verb=create,list,get,update,delete --resource=pods -n development
|
||||
kubectl create rolebinding developer --role developer -n development --user=john
|
||||
|
||||
|
||||
# ---------
|
||||
|
||||
kubectl run nginx-resolver --image=nginx
|
||||
kubectl expose pod nginx-resolver --port=80 --name=nginx-resolver-service
|
||||
|
||||
k run test-dns --image=busybox:1.28 --rm -it --restart=Never \
|
||||
-- nslookup nginx-resolver-service > /root/CKA/nginx.svc
|
||||
|
||||
k run test-dns --image=busybox:1.28 --rm -it --restart=Never -- nslookup 172-17-1-15.default.pod.cluster.local > /root/CKA/nginx.pod
|
||||
|
||||
# -----
|
||||
# nginx-critical
|
||||
|
||||
k run nginx-critical --image=nginx -o yaml --dry-run > /etc/kubernetes/manifests/nginx-critical.yaml
|
||||
|
||||
# ------
|
||||
# HPA
|
||||
kubectl autoscale deployment backend-deployment --min=3 --max=15 --memory=65% --name backend-hpa -n backend
|
||||
|
||||
237
kubernetes/mock exams/exam-3.md
Normal file
237
kubernetes/mock exams/exam-3.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# Task 1
|
||||
|
||||
```bash
|
||||
sysctl net.ipv4.ip_forward=1
|
||||
sysctl net.bridge.bridge-nf-call-iptables=1
|
||||
|
||||
# it is already persistent: /etc/sysctl.d/k8s.conf
|
||||
```
|
||||
|
||||
# Task 2
|
||||
|
||||
```bash
|
||||
kubectl create serviceaccount pvviewer
|
||||
kubectl create clusterrole pvviewer-role --verb=list --resource=persistentVolumes
|
||||
kubectl create clusterrolebinding pvviewer-role-binding --clusterrole=pvviewer-role --serviceaccount=default:pvviewer
|
||||
k run pvviewer --image=redis -o yaml --dry-run > p.yaml
|
||||
|
||||
# edit -> add serviceAccount -> apply
|
||||
|
||||
```
|
||||
|
||||
# Task 3
|
||||
|
||||
```bash
|
||||
cat sc.yaml
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
name: rancher-sc
|
||||
provisioner: rancher.io/local-path
|
||||
volumeBindingMode: WaitForFirstConsumer
|
||||
```
|
||||
|
||||
# Task 4
|
||||
|
||||
```bash
|
||||
kubectl create configmap app-config -n cm-namespace --from-litera
|
||||
l=ENV=production --from-literal=LOG_LEVEL=info
|
||||
|
||||
k get deployments.apps -n cm-namespace cm-webapp -o yaml | yq .spec
|
||||
progressDeadlineSeconds: 600
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 10
|
||||
selector:
|
||||
matchLabels:
|
||||
app: cm-webapp
|
||||
strategy:
|
||||
rollingUpdate:
|
||||
maxSurge: 25%
|
||||
maxUnavailable: 25%
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: cm-webapp
|
||||
spec:
|
||||
containers:
|
||||
- env:
|
||||
- name: LOG_LEVEL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: LOG_LEVEL
|
||||
name: app-config
|
||||
- name: ENV
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: ENV
|
||||
name: app-config
|
||||
image: nginx
|
||||
imagePullPolicy: Always
|
||||
name: nginx
|
||||
resources: {}
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
dnsPolicy: ClusterFirst
|
||||
restartPolicy: Always
|
||||
schedulerName: default-scheduler
|
||||
securityContext: {}
|
||||
terminationGracePeriodSeconds: 30
|
||||
```
|
||||
|
||||
# Task 5
|
||||
|
||||
```bash
|
||||
cat pc.yaml
|
||||
apiVersion: scheduling.k8s.io/v1
|
||||
kind: PriorityClass
|
||||
metadata:
|
||||
name: low-priority
|
||||
value: 50000
|
||||
```
|
||||
|
||||
# Task 6
|
||||
|
||||
```bash
|
||||
cat np.yaml
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: ingress-to-nptest
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
run: np-test-1
|
||||
ingress:
|
||||
- ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
policyTypes:
|
||||
- Ingress
|
||||
```
|
||||
|
||||
# Task 7
|
||||
|
||||
```bash
|
||||
k taint node node01 kodekloud:NoSchedule
|
||||
k run dev-redis --image redis:alpine
|
||||
k run prod-redis --image redis:alpine -o yaml --dry-run > p7.yaml
|
||||
|
||||
cat p7.yaml
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
labels:
|
||||
run: prod-redis
|
||||
name: prod-redis
|
||||
spec:
|
||||
containers:
|
||||
- image: redis:alpine
|
||||
name: prod-redis
|
||||
resources: {}
|
||||
dnsPolicy: ClusterFirst
|
||||
restartPolicy: Always
|
||||
tolerations:
|
||||
- key: "kodekloud"
|
||||
operator: "Exists"
|
||||
effect: "NoSchedule"
|
||||
```
|
||||
|
||||
# Task 8
|
||||
|
||||
```bash
|
||||
# k get storageClasses
|
||||
# fix accessMode
|
||||
kubectl get pvc app-pvc -n storage-ns -o yaml > pvc.yaml
|
||||
|
||||
cat pvc.yaml | yq .spec
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
volumeMode: Filesystem
|
||||
|
||||
```
|
||||
|
||||
# Task 9
|
||||
|
||||
```bash
|
||||
# fix port to api server in kubeconfig
|
||||
|
||||
```
|
||||
|
||||
# Task 10
|
||||
|
||||
```bash
|
||||
k scale deployment nginx-deploy --replicas=3
|
||||
|
||||
# /etc/kubernetes/manifests/controller-manager ... typo in command
|
||||
```
|
||||
|
||||
# Task 11
|
||||
|
||||
```bash
|
||||
# hpa
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: api-hpa
|
||||
namespace: api
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: api-deployment
|
||||
minReplicas: 1
|
||||
maxReplicas: 20
|
||||
metrics:
|
||||
- type: Pods
|
||||
pods:
|
||||
metric:
|
||||
name: requests_per_second
|
||||
target:
|
||||
type: AverageValue
|
||||
averageValue: "1000"
|
||||
```
|
||||
|
||||
# Task 12
|
||||
|
||||
```bash
|
||||
# httproute
|
||||
cat httproute.yaml
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: web-route
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: web-gateway
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /
|
||||
backendRefs:
|
||||
- name: web-service
|
||||
port: 80
|
||||
weight: 80
|
||||
- name: web-service-v2
|
||||
port: 80
|
||||
weight: 20
|
||||
```
|
||||
|
||||
# Task 13
|
||||
|
||||
```bash
|
||||
helm install webpage-server-02 /root/new-version/
|
||||
helm uninstall webpage-server-01
|
||||
```
|
||||
|
||||
# Task 14
|
||||
|
||||
```bash
|
||||
k get cm -n kube-system kubeadm-config -o yaml
|
||||
echo "172.17.0.0/16" > /root/pod-cidr.txt
|
||||
```
|
||||
328
kubernetes/mock exams/killer-sh-cka-1.md
Normal file
328
kubernetes/mock exams/killer-sh-cka-1.md
Normal file
@@ -0,0 +1,328 @@
|
||||
# Task 2
|
||||
|
||||
Solve this question on: ssh cks8930
|
||||
|
||||
The Vulnerability Scanner trivy is installed on your main terminal. Use it to scan the following images for known CVEs:
|
||||
|
||||
nginx:1.16.1-alpine
|
||||
|
||||
k8s.gcr.io/kube-apiserver:v1.18.0
|
||||
|
||||
k8s.gcr.io/kube-controller-manager:v1.18.0
|
||||
|
||||
docker.io/weaveworks/weave-kube:2.7.0
|
||||
|
||||
Write all image names (including the tag, exactly as listed above) that don't contain the vulnerabilities CVE-2020-10878 or CVE-2020-1967 into /opt/course/2/good-images on cks8930.
|
||||
|
||||
# Task 3
|
||||
|
||||
Controlling Access to the Kubernetes API
|
||||
|
||||
Solve this question on: ssh cks8930
|
||||
|
||||
You received a list from the DevSecOps team which performed a security investigation of the cluster. The list states the following about the apiserver setup:
|
||||
|
||||
Accessible through a NodePort Service
|
||||
Change the apiserver setup so that:
|
||||
|
||||
Only accessible through a ClusterIP Service
|
||||
ℹ️ Use sudo -i to become root which may be required for this question
|
||||
|
||||
# Task 4
|
||||
|
||||
Configure Service Accounts for Pods Managing Service Accounts
|
||||
|
||||
Solve this question on: ssh cks5608
|
||||
|
||||
Update file /opt/course/4/stream-multiplex.yaml with the following changes:
|
||||
|
||||
Pods should have annotation token-lifetime with value 1200
|
||||
ServiceAccount stream-multiplex should be used
|
||||
Disable automounting of ServiceAccount tokens
|
||||
The ServiceAccount token should be mounted at /var/run/secrets/custom/ with an expiration of 1200s
|
||||
Create the Deployment and ensure it's running without errors.
|
||||
|
||||
# Task 5
|
||||
|
||||
Securing a Cluster
|
||||
|
||||
Solve this question on: ssh cks7262
|
||||
|
||||
You're asked to evaluate specific settings of the cluster against the CIS Benchmark recommendations. Use the kube-bench tool which is already installed on the nodes.
|
||||
|
||||
Connect to the worker node using ssh cks7262-node1 from cks7262.
|
||||
|
||||
On the controlplane node ensure (correct if necessary) that the CIS recommendations are set for:
|
||||
|
||||
The --profiling argument of the kube-controller-manager
|
||||
|
||||
The ownership of directory /var/lib/etcd
|
||||
|
||||
On the worker node ensure (correct if necessary) that the CIS recommendations are set for:
|
||||
|
||||
The permissions of the kubelet configuration /var/lib/kubelet/config.yaml
|
||||
|
||||
The --client-ca-file argument of the kubelet
|
||||
|
||||
ℹ️ Use sudo -i to become root which may be required for this question
|
||||
|
||||
# Task 6
|
||||
|
||||
Configure a Security Context
|
||||
|
||||
Solve this question on: ssh cks2546
|
||||
|
||||
The Deployment immutable-deployment in Namespace team-purple should run immutable, it's created from file /opt/course/6/immutable-deployment.yaml on cks2546. Even after a successful break-in, it shouldn't be possible for an attacker to modify the filesystem of the running container.
|
||||
|
||||
Modify the Deployment in a way that no processes inside the container can modify the local filesystem, only /tmp directory should be writable. Don't modify the Docker image.
|
||||
|
||||
Save the updated YAML under /opt/course/6/immutable-deployment-new.yaml on cks2546 and update the running Deployment.
|
||||
|
||||
# Task 7
|
||||
|
||||
Pod Security Standards Pod Security Admission
|
||||
|
||||
Solve this question on: ssh cks5608
|
||||
|
||||
Implement specific security policies in Namespace team-sepia.
|
||||
|
||||
Configure Pod Security Admission in mode audit for level baseline
|
||||
|
||||
Configure Pod Security Admission in mode warn for level restricted
|
||||
|
||||
Afterwards create the Pod from /opt/course/7/bad-pod.yaml and write any warnings or errors into /opt/course/7/bad-pod.log
|
||||
|
||||
# Task 8
|
||||
|
||||
Solve this question on: ssh cks4024
|
||||
|
||||
Docker containers on cks4024 should run more isolated from each other by disabling inter-container communication.
|
||||
|
||||
Add "icc": false to the Docker config and ensure the Docker daemon is using the updated settings
|
||||
Create two Docker containers named container1 and container2 which should
|
||||
have image nginx:1-alpine
|
||||
restart always
|
||||
keep running in the background
|
||||
As result, the containers should not be able to ping each other on their IP addresses.
|
||||
|
||||
ℹ️ Run all Docker commands as root. Use sudo -i to become root
|
||||
|
||||
# Task 9
|
||||
|
||||
AppArmor
|
||||
|
||||
Solve this question on: ssh cks7262
|
||||
|
||||
Some containers need to run more secure and restricted. There is an existing AppArmor profile located at /opt/course/9/profile on cks7262 for this.
|
||||
|
||||
Install the AppArmor profile on node cks7262-node1.
|
||||
|
||||
Connect using ssh cks7262-node1 from cks7262
|
||||
|
||||
Add label security=apparmor to the node
|
||||
|
||||
Create a Deployment named apparmor in Namespace default with:
|
||||
|
||||
One replica of image nginx:1-alpine
|
||||
NodeSelector for security=apparmor
|
||||
Single container named c1 with the AppArmor profile enabled only for this container
|
||||
The Pod might not run properly with the profile enabled. Write the logs of the Pod into /opt/course/9/logs on cks7262 so another team can work on getting the application running.
|
||||
|
||||
ℹ️ Use sudo -i to become root which may be required for this question
|
||||
|
||||
# Task 10
|
||||
|
||||
Runtime Class
|
||||
|
||||
Solve this question on: ssh cks7262
|
||||
|
||||
Team purple wants to run some of their workloads more securely. Worker node cks7262-node1 is already configured so that containerd supports the runsc/gvisor runtime.
|
||||
|
||||
Connect to the worker node using ssh cks7262-node1 from cks7262.
|
||||
|
||||
Create a RuntimeClass named gvisor with handler runsc
|
||||
|
||||
Create a Pod that uses the RuntimeClass. The Pod should be in Namespace team-purple, named gvisor-test and of image nginx:1-alpine
|
||||
|
||||
Ensure the Pod only ever runs on a node named cks7262-node1
|
||||
|
||||
Write the output of the dmesg command of the successfully started Pod into /opt/course/10/gvisor-test-dmesg on cks7262
|
||||
|
||||
# Task 11
|
||||
|
||||
Secrets Managing Secrets using kubectl
|
||||
|
||||
Solve this question on: ssh cks2546
|
||||
|
||||
There is Secret db-con in Namespace team-khaki-us-east-ad1. Update the password to 4c!29f_Ee2e and ensure all Pods currently using the Secret will work with the updated value.
|
||||
|
||||
Move Secret user-data from Namespace team-khaki-us-east-ad1 to team-khaki-us-east-ad2.
|
||||
|
||||
Convert ConfigMap app-data in Namespace team-khaki-us-east-ad1 to a Secret and delete the ConfigMap afterwards. Ensure all Pods that used the ConfigMap will continue to work and are now using the values from the Secret.
|
||||
|
||||
# Task 12
|
||||
|
||||
Admission Control in Kubernetes
|
||||
|
||||
Solve this question on: ssh cks4024
|
||||
|
||||
Team White created an ImagePolicyWebhook solution at /opt/course/12/webhook on cks4024 which needs to be enabled for the cluster. There is an existing and working webhook-backend Service in Namespace team-white which will be the ImagePolicyWebhook backend.
|
||||
|
||||
Create an AdmissionConfiguration at /opt/course/12/webhook/admission-config.yaml which contains the following ImagePolicyWebhook configuration in the same file:
|
||||
|
||||
imagePolicy:
|
||||
kubeConfigFile: /etc/kubernetes/webhook/webhook.yaml
|
||||
allowTTL: 10
|
||||
denyTTL: 10
|
||||
retryBackoff: 20
|
||||
defaultAllow: true
|
||||
Configure the apiserver to:
|
||||
|
||||
Mount /opt/course/12/webhook at /etc/kubernetes/webhook
|
||||
|
||||
Use the AdmissionConfiguration at path /etc/kubernetes/webhook/admission-config.yaml
|
||||
|
||||
Enable the ImagePolicyWebhook admission plugin
|
||||
|
||||
As result the ImagePolicyWebhook backend should prevent container images containing danger-danger from being used, any other image should still work.
|
||||
|
||||
ℹ️ Create a backup of /etc/kubernetes/manifests/kube-apiserver.yaml outside of /etc/kubernetes/manifests so you can revert back in case of issues
|
||||
|
||||
ℹ️ Use sudo -i to become root which may be required for this question
|
||||
|
||||
# Task 13
|
||||
|
||||
Cilium Documentation
|
||||
|
||||
Solve this question on: ssh cks8930
|
||||
|
||||
There is a metadata service available at http://192.168.100.21:9055 on which nodes can reach sensitive data. Access to this needs to be restricted from Pods.
|
||||
|
||||
In Namespace metadata-access create a CiliumNetworkPolicy named default to:
|
||||
|
||||
Allow egress to 0.0.0.0/0
|
||||
Allow egress to Endpoints in the same Namespace
|
||||
Allow egress to Endpoints in the kube-system Namespace
|
||||
Deny egress to 192.168.100.21 on port 9055
|
||||
ℹ️ There are existing plain Nginx Pods with open port 80 in the Namespace which can be used for testing but need to remain unchanged. Perform simple connectivity tests like:
|
||||
|
||||
k -n metadata-access exec POD_NAME -- curl URL
|
||||
|
||||
# Task 14
|
||||
|
||||
Encrypting Confidential Data at Rest
|
||||
|
||||
Solve this question on: ssh cks7262
|
||||
|
||||
An internal security audit requires to have secrets in the cluster encrypted. The team already created the needed EncryptionConfiguration at /etc/kubernetes/etcd/ec.yaml.
|
||||
|
||||
Write the non-encoded password that the aesgcm provider of that EncryptionConfiguration uses into /opt/course/14/password.txt
|
||||
The Apiserver should mount /etc/kubernetes/etcd on the host to /etc/kubernetes/etcd inside the container
|
||||
The Apiserver should use the EncryptionConfiguration from /etc/kubernetes/etcd/ec.yaml inside the container
|
||||
All Secrets in Namespace team-magenta should be stored encrypted in ETCD
|
||||
|
||||
# Task 15
|
||||
|
||||
Ingress
|
||||
|
||||
Solve this question on: ssh cks2546
|
||||
|
||||
In Namespace team-pink there is an existing Nginx Ingress resource named secure which accepts two paths /app and /api which point to different ClusterIP Services.
|
||||
|
||||
From your main terminal you can connect to it using for example:
|
||||
|
||||
HTTP: curl -v http://secure-ingress.test:31080/app
|
||||
|
||||
HTTPS: curl -kv https://secure-ingress.test:31443/app
|
||||
|
||||
Right now it uses a default generated TLS certificate by the Nginx Ingress Controller.
|
||||
|
||||
You're asked to instead use the key and certificate provided at /opt/course/15/tls.key and /opt/course/15/tls.crt. As it's a self-signed certificate you need to use curl -k when connecting to it.
|
||||
|
||||
# Task 16
|
||||
Falco Documentation
|
||||
|
||||
Solve this question on: ssh cks5608
|
||||
|
||||
Add two new Falco rules to /etc/falco/falco_rules.local.yaml:
|
||||
|
||||
Named Custom Rule 1 with priority WARNING. It should find all containers that access files on the host with prefix /etc/kubernetes in the full path. It should output logs as:
|
||||
|
||||
custom_rule_1 file={{FILEPATH}} container={{CONTAINER_ID}}
|
||||
Named Custom Rule 2 with priority INFO. It should find all processes that perform kill syscalls. It should output logs as:
|
||||
|
||||
custom_rule_2 event_signal=%evt.arg.sig event_pid=%evt.arg.pid container={{CONTAINER_ID}}
|
||||
Only create the new rules without additional macros or lists.
|
||||
|
||||
Run Falco with your implemented rules for at least 30 seconds and write the produced logs into /opt/course/16/logs.
|
||||
|
||||
# Task 17
|
||||
|
||||
Auditing
|
||||
|
||||
Solve this question on: ssh cks3477
|
||||
|
||||
Audit Logging has been enabled in the cluster with an Audit Policy located at /etc/kubernetes/audit/policy.yaml on cks3477.
|
||||
|
||||
Change the configuration so that only one backup of the logs is stored.
|
||||
|
||||
Alter the Policy in a way that it only stores logs:
|
||||
|
||||
From Secret resources, level Metadata
|
||||
From "system:nodes" userGroups, level RequestResponse
|
||||
After you update the Policy make sure to empty the log file so it only contains entries according to your changes, like using echo > /etc/kubernetes/audit/logs/audit.log.
|
||||
|
||||
ℹ️ You can use yq to render JSON more readable, like cat data.json | yq -p json -o json
|
||||
|
||||
ℹ️ Use sudo -i to become root which may be required for this question
|
||||
|
||||
# Preview 1
|
||||
|
||||
Preview Question 1
|
||||
Using RBAC Authorization
|
||||
|
||||
Solve this question on: ssh cks3477
|
||||
|
||||
You're asked to implement some RBAC for user gianna:
|
||||
|
||||
There are existing cluster-level RBAC resources in place to, among other things, ensure that user gianna can never read Secret contents cluster-wide. Confirm this is correct or restrict the existing RBAC resources to ensure this.
|
||||
|
||||
In addition, create more RBAC resources to allow user gianna to create Pods and Deployments in Namespaces security, restricted and internal. It's likely the user will receive these exact permissions as well for other Namespaces in the future.
|
||||
|
||||
To test your RBAC you can:
|
||||
|
||||
Switch to the other context with:
|
||||
|
||||
k config use-context gianna@infra-prod
|
||||
And afterwards switch back to the default context with:
|
||||
|
||||
k config use-context kubernetes-admin@kubernetes
|
||||
|
||||
|
||||
# Preview 2
|
||||
|
||||
Preview Question 2
|
||||
Auditing Managing Secrets using kubectl
|
||||
|
||||
Solve this question on: ssh cks3477
|
||||
|
||||
Namespace security contains five Secrets of type Opaque which can be considered highly confidential. The latest Incident-Prevention-Investigation revealed that ServiceAccount p.auster had too broad access to the cluster for some time. This SA should never have had access to any Secrets in that Namespace.
|
||||
|
||||
Find out which Secrets in Namespace security this SA did access by looking at the Audit Logs under /opt/course/p2/audit.log.
|
||||
|
||||
Change the password to any new string of only those Secrets that were accessed by this SA.
|
||||
|
||||
ℹ️ You can use jq to render json more readable, like cat data.json | jq
|
||||
|
||||
|
||||
|
||||
# Preview 3
|
||||
|
||||
Preview Question 3
|
||||
Solve this question on: ssh cks8930
|
||||
|
||||
A security scan result shows that there is an unknown miner process running on one of the nodes in this cluster.
|
||||
|
||||
The report states that the process is listening on port 6666.
|
||||
|
||||
Kill the process and delete the binary.
|
||||
0
kubernetes/mock exams/killer-sh-cka-2.md
Normal file
0
kubernetes/mock exams/killer-sh-cka-2.md
Normal file
Reference in New Issue
Block a user