docs: add Kubernetes CKS study notes
This commit is contained in:
137
kubernetes/cks/cks-guide-1.md
Normal file
137
kubernetes/cks/cks-guide-1.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# CKS Edition — Resources & Sprint Plan
|
||||||
|
|
||||||
|
> **Owner:** freshly-minted CKA (81%, v1.35 curriculum), prod SRE
|
||||||
|
> **Mission:** CKS while the cluster-surgery fingers are still warm
|
||||||
|
> **Doc status:** v0.1 — living document
|
||||||
|
> **Operating mode unchanged:** DRILL > read. Type first, autopsy second.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Ground truth
|
||||||
|
|
||||||
|
| Item | Value |
|
||||||
|
|---|---|
|
||||||
|
| Prerequisite | Valid-at-any-point CKA — ✅ you qualify twice over |
|
||||||
|
| Format | 15–20 tasks (typically ~16), 120 min, PSI remote proctored |
|
||||||
|
| Passing score | **67%** (one point higher than CKA) |
|
||||||
|
| Exam version | Tracks latest k8s minor — verify at booking |
|
||||||
|
| Voucher includes | 1 free retake + **2 killer.sh CKS sessions** |
|
||||||
|
| killer.sh CKS quirk | Both sessions have **IDENTICAL questions** (unlike CKA's A/B split) — changes the protocol, see §4 |
|
||||||
|
| Curriculum revision | Oct 2024 — courses/notes older than that are poison |
|
||||||
|
|
||||||
|
### Domains (post-2024 weights)
|
||||||
|
- Cluster Setup — 10%
|
||||||
|
- Cluster Hardening — 15%
|
||||||
|
- System Hardening — 15%
|
||||||
|
- Minimize Microservice Vulnerabilities — 20%
|
||||||
|
- Supply Chain Security — 20%
|
||||||
|
- Monitoring, Logging & Runtime Security — 20%
|
||||||
|
|
||||||
|
### Allowed docs (broader than CKA — this is a gift)
|
||||||
|
- kubernetes.io/docs + kubernetes.io/blog
|
||||||
|
- **Third-party tool docs for exam-covered tools**: Falco, Trivy, AppArmor (etc. — verify exact list on the LF exam-resources page at booking)
|
||||||
|
- One extra browser tab, same PSI environment as CKA — you know the desk already
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Resource stack — ranked for YOUR situation
|
||||||
|
|
||||||
|
### Tier 1 — the spine (use all three)
|
||||||
|
|
||||||
|
**1. KodeKloud CKS course (Mumshad)** — primary, already in your sub, fully updated for the Oct-2024 revision.
|
||||||
|
Usage: unlike your CKA re-sit, this is NEW material — watch + lab for everything except RBAC/NetworkPolicy/ServiceAccount sections (skim-lab those; you just aced them). The community verdict is consistent: KodeKloud CKS alone covers what the exam requires.
|
||||||
|
|
||||||
|
**2. Kim Wüstkamp's free CKS course (YouTube) + Killercoda CKS scenarios (free)** — the resource CKA prep didn't have an equivalent of. Made by the killer.sh author, so it's tuned to exactly how the simulator (and therefore the exam) phrases things.
|
||||||
|
Usage: Killercoda scenarios as your DAILY drill snack — browser-based, zero setup, one scenario per coffee. The video course as second-angle explanation when a KodeKloud topic doesn't click (Falco rules and AppArmor profiles are the usual candidates).
|
||||||
|
|
||||||
|
**3. killer.sh CKS sessions (with voucher)** — same diagnostic role as before, one protocol change (§4).
|
||||||
|
|
||||||
|
### Tier 2 — in your sub, use selectively
|
||||||
|
|
||||||
|
**KodeKloud "CKS Challenges"** — scenario-based hardening gauntlets (attack-then-defend style). Excellent as the bridge between course labs and mocks; run these AFTER the course, BEFORE killer.
|
||||||
|
**KodeKloud Ultimate CKS Mock Exam Series** — auto-scored mocks mapped to curriculum proportions. Same role as the CKA Ultimate mocks: volume + speed gate before killer. Same caveats as before: question-bank recycling on repeats, occasional wrong answers — grade skeptically.
|
||||||
|
|
||||||
|
### Tier 3 — reference, not courses
|
||||||
|
|
||||||
|
- **Official curriculum repo** (github.com/cncf/curriculum) — read the CKS PDF once; it's the contract.
|
||||||
|
- **kube-bench / CIS benchmark output** — you don't study CIS docs; you run kube-bench and learn to read its remediation text (that's the exam skill).
|
||||||
|
- Community question repos (e.g. the killer.sh-style GitHub collections) — mine for extra drill ideas ONLY after killer; don't let 2020-era questions teach you PSP or other dead APIs.
|
||||||
|
|
||||||
|
### Skip
|
||||||
|
- LFS260 (official LF course) — fine content, but redundant with KodeKloud at extra cost.
|
||||||
|
- Any material predating Oct 2024 revision without an update note. PSP, old dashboards — dead ends.
|
||||||
|
- Paid mock bundles beyond what's above. Volume is already covered.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. The delta (what CKA didn't teach you)
|
||||||
|
|
||||||
|
Tiered by exam weight × your unfamiliarity:
|
||||||
|
|
||||||
|
### Tier 1 — new tools, guaranteed presence, drill until boring
|
||||||
|
- [ ] **Falco** (Runtime, 20% domain) — read/write rules, find the offending pod from log output, output_fields formatting, `falco.yaml` config, service restart flow. Killer's classic: "collect logs in EXACTLY this format."
|
||||||
|
- [ ] **Trivy** (Supply chain, 20%) — image scanning, filtering by CVE ID/severity, scanning many images fast (`k get pods -o jsonpath` → loop trivy)
|
||||||
|
- [ ] **AppArmor + seccomp** (System hardening) — load a profile on the node (`apparmor_parser`), reference via `securityContext.appArmorProfile` (post-1.30 field, NOT the old annotation — revision trap), seccomp `RuntimeDefault` + custom Localhost profiles
|
||||||
|
- [ ] **Audit logging** — write an audit policy (levels, rules, omitStages), wire `--audit-*` flags + volumes into the apiserver static pod, grep/jq the log
|
||||||
|
- [ ] **Admission for images** — ImagePolicyWebhook flag/config plumbing; plus awareness of OPA Gatekeeper/Kyverno constraint shapes
|
||||||
|
|
||||||
|
### Tier 2 — extensions of what you know
|
||||||
|
- [ ] **Pod Security Admission** — namespace labels, enforce/audit/warn, baseline vs restricted (you know the concept; drill the label syntax cold)
|
||||||
|
- [ ] **SecurityContext deep cuts** — runAsNonRoot, readOnlyRootFilesystem, capabilities drop/add, privileged escalation chain
|
||||||
|
- [ ] **NetworkPolicy expert tier** — default-deny both directions + selective holes; you're already strong, push to speed
|
||||||
|
- [ ] **RBAC hardening** — minimize/audit existing permissions (reverse of creating them), SA token automounting off, bound tokens
|
||||||
|
- [ ] **Secrets encryption at rest** — EncryptionConfiguration, apiserver flag, re-encrypt existing secrets
|
||||||
|
- [ ] **TLS/cert hygiene** — apiserver TLS versions/ciphers flags, kubelet authn/authz flags (the x509 muscle from CKA transfers directly)
|
||||||
|
- [ ] **RuntimeClass / gVisor** — define RuntimeClass, run pod under runsc, verify via `dmesg`/uname trick
|
||||||
|
- [ ] **kube-bench** — run, read, remediate selected findings on CP node
|
||||||
|
- [ ] **mTLS / ServiceMesh awareness** — curriculum mentions it at concept level; don't over-invest
|
||||||
|
|
||||||
|
### Tier 3 — CKA muscle that transfers as-is
|
||||||
|
Static pod surgery, kubeadm layout, etcd, kubeconfig triage, netpol, RBAC creation, jsonpath — all directly reusable. Your v2 mock pack was secretly half CKS prep.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. killer.sh protocol — CHANGED for CKS
|
||||||
|
|
||||||
|
Both CKS sessions are **identical** (same 17 questions), unlike CKA's different A/B sets. So:
|
||||||
|
- **Session 1 (T-14 to T-10):** full 120-min diagnostic, exam conditions. Then the 34h autopsy: every solution, re-solve misses.
|
||||||
|
- **Session 2 (T-3):** it's a RETAKE of the same paper — the goal is not discovery but **fluency**: target 90%+ and finishing with 20+ min spare. It measures whether the patch work landed, not whether you'd pass cold.
|
||||||
|
- Between them: Killercoda scenarios + KodeKloud mocks fill the novelty gap that CKA's different-question session 2 used to provide.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Sprint plan — 3 weeks (CKA-holder pace)
|
||||||
|
|
||||||
|
### Week 1 — Tooling assault
|
||||||
|
- D1–2: Falco (KodeKloud section + labs + 2 Killercoda scenarios)
|
||||||
|
- D3: Trivy + supply chain (image digests, allowed registries via policy)
|
||||||
|
- D4: AppArmor + seccomp end-to-end (node profile → pod reference → verify)
|
||||||
|
- D5: Audit logging (policy authoring + apiserver wiring — this is static-pod surgery you already own)
|
||||||
|
- Weekend: PSA + SecurityContext + secrets encryption; first KodeKloud CKS mock, untimed, to map the terrain
|
||||||
|
|
||||||
|
### Week 2 — Integration
|
||||||
|
- D1: kube-bench + apiserver/kubelet hardening flags
|
||||||
|
- D2: ImagePolicyWebhook + admission landscape
|
||||||
|
- D3: RuntimeClass/gVisor + RBAC minimization drills
|
||||||
|
- D4–5: KodeKloud CKS Challenges (the attack/defend gauntlets)
|
||||||
|
- Weekend: **killer.sh CKS session 1** under full exam law → miss list → Claude drill pack CKS-edition gets built from it
|
||||||
|
|
||||||
|
### Week 3 — Sharpen + sit
|
||||||
|
- D1–3: drills on session-1 misses; Ultimate CKS mocks timed until <100 min clean
|
||||||
|
- D4: **killer.sh session 2** (the fluency retake — 90%+ or investigate)
|
||||||
|
- D5–6: docs-navigation reps on Falco/Trivy/AppArmor sites (new allowed domains = new muscle), light review
|
||||||
|
- D7: **EXAM**
|
||||||
|
|
||||||
|
Range note: the kind+Cilium `drills` cluster handles ~70% of CKS drilling (netpol, PSA, securityContext, RBAC, audit-policy authoring, RuntimeClass definitions, trivy/kube-bench runs). AppArmor/Falco/gVisor need real node access — kind containers can host trivy and kube-bench fine, AppArmor partially (host kernel dependent); use KodeKloud labs/Killercoda for the kernel-adjacent stuff rather than fighting the range.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Booking checklist
|
||||||
|
- [ ] CKS purchased (includes retake + 2 killer sessions)
|
||||||
|
- [ ] Exam date booked: ______ (target: ~3.5 weeks out, per sprint)
|
||||||
|
- [ ] killer.sh CKS sessions visible in portal
|
||||||
|
- [ ] LF allowed-resources page re-checked at booking (tool-docs list)
|
||||||
|
- [ ] Dog scheduled for grandma's house on exam day — **non-negotiable line item**
|
||||||
|
|
||||||
|
## 7. Changelog
|
||||||
|
- v0.1 — initial: ground truth, ranked resource stack, delta inventory, killer protocol correction, 3-week sprint.
|
||||||
37
kubernetes/cks/exam-by-claude/README.md
Normal file
37
kubernetes/cks/exam-by-claude/README.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# CKS Mock Exam — k8s v1.35
|
||||||
|
|
||||||
|
Timed practice that mirrors the current CKS (17 tasks, 2h, six domains). Runs on kind (k8s v1.35.5) with the host-side / kernel bits on your Ubuntu VM.
|
||||||
|
|
||||||
|
## Prereqs (Ubuntu VM)
|
||||||
|
- docker, `kind >= v0.32.0`, `kubectl` v1.35, `helm`
|
||||||
|
- host tools installed latest: `kube-bench`, `trivy`, `kubesec`, `falco` (v0.44.x), optional `cosign`
|
||||||
|
- host must have AppArmor enabled (default on Ubuntu) for Task 7
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
```bash
|
||||||
|
chmod +x bootstrap.sh seed.sh
|
||||||
|
./bootstrap.sh # creates 'cks' cluster (1 cp + 2 workers) + ingress-nginx
|
||||||
|
./seed.sh # plants the target/vulnerable objects
|
||||||
|
# set a 2h timer, open exam.md, go.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files
|
||||||
|
- `kind-config.yaml` — cluster topology, v1.35.5 pinned digest, exam-files mount
|
||||||
|
- `bootstrap.sh` — cluster + ingress controller + tooling checklist
|
||||||
|
- `seed.sh` — objects for the kubectl-only tasks
|
||||||
|
- `exam.md` — the 17 tasks (per-task setup + statement, no answers)
|
||||||
|
- `answers/solutions.md` — worked solutions (don't peek until you've timed a full run)
|
||||||
|
|
||||||
|
## Domain coverage (weights)
|
||||||
|
Cluster Setup 15 · Cluster Hardening 15 · System Hardening 10 · Microservice Vuln 20 · Supply Chain 20 · Monitoring/Logging/Runtime 20.
|
||||||
|
|
||||||
|
## Reset
|
||||||
|
```bash
|
||||||
|
kind delete cluster --name cks && ./bootstrap.sh && ./seed.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes / kind caveats
|
||||||
|
- **Version:** exam is on v1.35 per the Linux Foundation page. k8s 1.36 shipped ~May 2026; the exam env aligns "within 4–8 weeks" of a release, so it *may* have rolled to 1.36 by your date — verify on the LF exam page. Nothing in this set changes between 1.35/1.36. To bump: swap the digest in `kind-config.yaml` for a `kindest/node:v1.36.x` one.
|
||||||
|
- **AppArmor** needs a real AppArmor host; works on Ubuntu, not on Docker Desktop/macOS.
|
||||||
|
- **gVisor (Task 11)** pod stays Pending in kind (no `runsc` in kind's containerd) — the config is the graded artifact.
|
||||||
|
- **ImagePolicyWebhook / audit / encryption** edit the live apiserver: always `cp` the manifest first so you can revert fast.
|
||||||
33
kubernetes/cks/exam-by-claude/bootstrap.sh
Normal file
33
kubernetes/cks/exam-by-claude/bootstrap.sh
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# CKS mock — cluster bootstrap. Run on your Ubuntu VM (needs docker, kind >= v0.32.0, kubectl v1.35, helm).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$HERE"
|
||||||
|
|
||||||
|
mkdir -p exam-files
|
||||||
|
|
||||||
|
echo "[*] Creating kind cluster 'cks' (1 cp + 2 workers, k8s v1.35.5)..."
|
||||||
|
kind create cluster --config kind-config.yaml
|
||||||
|
kubectl config use-context kind-cks
|
||||||
|
|
||||||
|
echo "[*] Installing an ingress controller (needed for the TLS task)..."
|
||||||
|
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
|
||||||
|
kubectl -n ingress-nginx wait --for=condition=Ready pod \
|
||||||
|
-l app.kubernetes.io/component=controller --timeout=180s || true
|
||||||
|
|
||||||
|
cat <<'EOF'
|
||||||
|
|
||||||
|
[*] Cluster up. Sanity check:
|
||||||
|
kubectl get nodes -o wide
|
||||||
|
|
||||||
|
[*] Host-side tooling you want on the VM (install latest):
|
||||||
|
- kube-bench : https://github.com/aquasecurity/kube-bench/releases (or run as a Job in-cluster)
|
||||||
|
- trivy : https://github.com/aquasecurity/trivy (apt: aquasecurity repo)
|
||||||
|
- kubesec : https://github.com/controlplaneio/kubesec/releases
|
||||||
|
- falco : apt install falco (v0.44.1) — used in the runtime task on the VM
|
||||||
|
- helm : for Kyverno / Falco chart installs
|
||||||
|
- cosign : optional, image signing task
|
||||||
|
|
||||||
|
[*] Now run ./seed.sh to plant the vulnerable/target objects for the kubectl-only tasks.
|
||||||
|
EOF
|
||||||
344
kubernetes/cks/exam-by-claude/exam.md
Normal file
344
kubernetes/cks/exam-by-claude/exam.md
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
# CKS Mock Exam — k8s v1.35
|
||||||
|
|
||||||
|
**Format mirrors the real thing:** 17 tasks, 2 hours, ~66% to pass. Each task shows a weight and a domain. In the real exam every task tells you which cluster/context to `kubectl config use-context` into first — here it's all one cluster (`kind-cks`), but the habit of *reading the context line first* is baked in anyway. Tasks touching control-plane files run via `docker exec cks-control-plane ...`; host/kernel tasks run on the node container or your Ubuntu VM as noted.
|
||||||
|
|
||||||
|
**Ground rules (same as real exam):**
|
||||||
|
- Bookmark allowed docs only: kubernetes.io/docs, kubernetes.io/blog, falco.org, and the tool projects. Practice with those tabs only.
|
||||||
|
- No stopping the clock. Do the fast, formulaic tasks first (NetworkPolicy, RBAC, PSA labels), leave apiserver-restart tasks for a block where you can afford the ~1 min downtime.
|
||||||
|
- Every task: `kubectl config use-context kind-cks` before you start (muscle memory).
|
||||||
|
|
||||||
|
Setup: `./bootstrap.sh` then `./seed.sh`. Per-task extra setup is in each **Setup** block below.
|
||||||
|
|
||||||
|
Weight tally: Cluster Setup 15 · Cluster Hardening 15 · System Hardening 10 · Microservice Vuln 20 · Supply Chain 20 · Monitoring/Logging/Runtime 20.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1 — Default-deny + selective NetworkPolicy · Cluster Setup · 4%
|
||||||
|
|
||||||
|
**Context:** `kind-cks`
|
||||||
|
|
||||||
|
**Setup:** seeded by `seed.sh` (namespace `prod`: `db`, `frontend`, `attacker`).
|
||||||
|
|
||||||
|
**Task:**
|
||||||
|
In namespace `prod`, the `db` workload must only be reachable from the `frontend` workload.
|
||||||
|
|
||||||
|
1. Create a NetworkPolicy `default-deny` in `prod` that denies **all ingress** to every pod in the namespace.
|
||||||
|
2. Create a NetworkPolicy `allow-frontend` in `prod` that allows ingress to pods labelled `app=db` **only** from pods labelled `app=frontend`, on TCP 5432.
|
||||||
|
|
||||||
|
Do not modify the deployments. `attacker` must NOT be able to reach `db`; `frontend` must.
|
||||||
|
|
||||||
|
> Verify: exec into `frontend` and `attacker`, `curl db:5432` — frontend succeeds, attacker times out.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2 — TLS-terminated Ingress · Cluster Setup · 4%
|
||||||
|
|
||||||
|
**Context:** `kind-cks`
|
||||||
|
|
||||||
|
**Setup:** seeded (`web` ns: `hello` Deployment + Service). ingress-nginx installed by bootstrap.
|
||||||
|
|
||||||
|
**Task:**
|
||||||
|
Expose the `hello` Service in namespace `web` over HTTPS.
|
||||||
|
|
||||||
|
1. Create a TLS Secret `hello-tls` in `web` from a self-signed cert for host `hello.cks.local` (you generate the cert/key on the VM).
|
||||||
|
2. Create an Ingress `hello` in `web` routing host `hello.cks.local` path `/` to Service `hello:80`, using `hello-tls` for TLS.
|
||||||
|
|
||||||
|
> Verify: `curl -k --resolve hello.cks.local:443:127.0.0.1 https://hello.cks.local/` returns `hello-tls` (adjust port to the ingress-nginx NodePort/hostPort in your setup).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3 — kube-bench: remediate CIS findings · Cluster Setup · 5%
|
||||||
|
|
||||||
|
**Context:** `kind-cks` (control-plane node)
|
||||||
|
|
||||||
|
**Setup:** none beyond the cluster.
|
||||||
|
|
||||||
|
**Task:**
|
||||||
|
Run `kube-bench` against the control-plane node and remediate the following **FAIL** items so a re-run passes:
|
||||||
|
|
||||||
|
- kubelet: `--anonymous-auth` must be `false`.
|
||||||
|
- kubelet: `--authorization-mode` must not be `AlwaysAllow` (use `Webhook`).
|
||||||
|
- kubelet: read-only port must be disabled (`--read-only-port=0`).
|
||||||
|
- kube-apiserver: `--profiling` must be `false`.
|
||||||
|
|
||||||
|
Apply the kubelet changes via the kubelet config on the node and restart kubelet; apply the apiserver change via the static manifest. Re-run kube-bench to confirm those four checks pass.
|
||||||
|
|
||||||
|
> The kubelet config on a kind node is `/var/lib/kubelet/config.yaml`; the apiserver manifest is `/etc/kubernetes/manifests/kube-apiserver.yaml`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4 — RBAC least privilege · Cluster Hardening · 5%
|
||||||
|
|
||||||
|
**Context:** `kind-cks`
|
||||||
|
|
||||||
|
**Setup:** seeded (`dev` ns: SA `ci-runner`, Role `ci-runner-role` with `*/*/*`, RoleBinding `ci-runner-rb`).
|
||||||
|
|
||||||
|
**Task:**
|
||||||
|
The `ci-runner` ServiceAccount in `dev` is bound to a cluster-admin-equivalent Role. Restrict it to least privilege:
|
||||||
|
|
||||||
|
- `ci-runner` must be able to **get, list, watch** only `pods` and `configmaps` in `dev`.
|
||||||
|
- It must have **no other permissions** anywhere.
|
||||||
|
|
||||||
|
Do not delete the SA or the RoleBinding — reshape the Role (or replace it) so the binding still points at the right role name. Verify with `kubectl auth can-i`.
|
||||||
|
|
||||||
|
> Verify: `kubectl auth can-i delete pods --as=system:serviceaccount:dev:ci-runner -n dev` → **no**; `... get pods ...` → **yes**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5 — API server hardening · Cluster Hardening · 5%
|
||||||
|
|
||||||
|
**Context:** `kind-cks` (control-plane)
|
||||||
|
|
||||||
|
**Setup:** none.
|
||||||
|
|
||||||
|
**Task:**
|
||||||
|
Edit the kube-apiserver static manifest so that:
|
||||||
|
|
||||||
|
1. Anonymous auth is disabled (`--anonymous-auth=false`).
|
||||||
|
2. The `NodeRestriction` admission plugin is enabled.
|
||||||
|
3. Profiling is disabled (`--profiling=false`) — if you already did this in Task 3, confirm it's present.
|
||||||
|
|
||||||
|
Wait for the apiserver static pod to come back healthy before moving on.
|
||||||
|
|
||||||
|
> Verify: `kubectl -n kube-system get pod -l component=kube-apiserver` Running; `curl -k https://127.0.0.1:6443/healthz` from inside the node returns anonymous 401/403 rather than `ok`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6 — Disable ServiceAccount token automount · Cluster Hardening · 5%
|
||||||
|
|
||||||
|
**Context:** `kind-cks`
|
||||||
|
|
||||||
|
**Setup:** seeded (`apps` ns: SA `web-sa`, Deployment `web`).
|
||||||
|
|
||||||
|
**Task:**
|
||||||
|
Pods in namespace `apps` should not receive an automounted ServiceAccount token unless they explicitly need one.
|
||||||
|
|
||||||
|
1. Set `automountServiceAccountToken: false` on the `web-sa` ServiceAccount.
|
||||||
|
2. Ensure the running `web` pods no longer mount the token (roll them if needed).
|
||||||
|
|
||||||
|
Do not change the ServiceAccount name or the Deployment's `serviceAccountName`.
|
||||||
|
|
||||||
|
> Verify: `kubectl -n apps exec deploy/web -- ls /var/run/secrets/kubernetes.io/serviceaccount` → no such file/dir.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7 — AppArmor profile enforcement · System Hardening · 5%
|
||||||
|
|
||||||
|
**Context:** `kind-cks` (worker node + cluster)
|
||||||
|
|
||||||
|
**Setup:** run this to stage the profile file on a worker (host must have AppArmor — Ubuntu VM does):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# on the VM
|
||||||
|
cat > /tmp/deny-write <<'EOF'
|
||||||
|
#include <tunables/global>
|
||||||
|
profile k8s-deny-write flags=(attach_disconnected) {
|
||||||
|
#include <abstractions/base>
|
||||||
|
file,
|
||||||
|
deny /** w,
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
docker cp /tmp/deny-write cks-worker:/root/deny-write
|
||||||
|
```
|
||||||
|
|
||||||
|
**Task:**
|
||||||
|
1. Load the AppArmor profile `k8s-deny-write` in **enforce** mode on node `cks-worker`.
|
||||||
|
2. Create a Pod `locked` in namespace `sysh` running `image: busybox:1.36`, command `sleep`→ use `["sh","-c","sleep 1h"]`, that runs under the `k8s-deny-write` profile using the **AppArmor securityContext API** (not the deprecated annotation). Pin it to `cks-worker`.
|
||||||
|
|
||||||
|
> Verify: `kubectl -n sysh exec locked -- sh -c 'echo x > /tmp/x'` → **Permission denied**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 8 — seccomp custom profile · System Hardening · 5%
|
||||||
|
|
||||||
|
**Context:** `kind-cks` (worker node + cluster)
|
||||||
|
|
||||||
|
**Setup:** stage a custom seccomp profile on the worker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat > /tmp/audit.json <<'EOF'
|
||||||
|
{ "defaultAction": "SCMP_ACT_LOG" }
|
||||||
|
EOF
|
||||||
|
docker exec cks-worker mkdir -p /var/lib/kubelet/seccomp/profiles
|
||||||
|
docker cp /tmp/audit.json cks-worker:/var/lib/kubelet/seccomp/profiles/audit.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Task:**
|
||||||
|
Create a Pod `traced` in namespace `sysh` (`image: busybox:1.36`, `["sh","-c","sleep 1h"]`, pinned to `cks-worker`) that uses the **Localhost** seccomp profile `profiles/audit.json`. Then create a second Pod `defaulted` in `sysh` that uses the `RuntimeDefault` seccomp profile.
|
||||||
|
|
||||||
|
> Verify: `kubectl -n sysh get pod traced -o jsonpath='{.spec.securityContext.seccompProfile}'` shows the Localhost profile.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 9 — Enforce restricted Pod Security Standard · Microservice Vuln · 5%
|
||||||
|
|
||||||
|
**Context:** `kind-cks`
|
||||||
|
|
||||||
|
**Setup:** seeded (`restricted-ns`: Deployment `payments` that is privileged → violates restricted).
|
||||||
|
|
||||||
|
**Task:**
|
||||||
|
1. Label namespace `restricted-ns` to **enforce** the `restricted` Pod Security Standard (latest version), and to **warn/audit** at `restricted` too.
|
||||||
|
2. The existing `payments` Deployment currently violates it. Modify the pod template so it complies with `restricted` (drop privilege, set the required securityContext fields) and rolls out successfully.
|
||||||
|
|
||||||
|
> Verify: `kubectl -n restricted-ns get deploy payments` has ready replicas; creating a privileged pod in the ns is rejected.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 10 — Encrypt Secrets at rest · Microservice Vuln · 6%
|
||||||
|
|
||||||
|
**Context:** `kind-cks` (control-plane)
|
||||||
|
|
||||||
|
**Setup:** create a secret that currently sits in etcd in plaintext:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl create ns vault-ns --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
kubectl -n vault-ns create secret generic pre-existing --from-literal=api=SUPERSECRET
|
||||||
|
```
|
||||||
|
|
||||||
|
**Task:**
|
||||||
|
1. Configure an `EncryptionConfiguration` (provider `aescbc` with a 32-byte key, `identity` as fallback) at `/etc/kubernetes/enc/enc.yaml` on the control-plane, and point the apiserver at it via `--encryption-provider-config` (mount it into the static pod).
|
||||||
|
2. Ensure **all existing Secrets** cluster-wide are rewritten so they're stored encrypted.
|
||||||
|
|
||||||
|
> Verify: read the raw etcd value for `secret/vault-ns/pre-existing` with `etcdctl` inside the control-plane — it must start with `k8s:enc:aescbc:` and not contain `SUPERSECRET`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 11 — RuntimeClass for sandboxed workload · Microservice Vuln · 4%
|
||||||
|
|
||||||
|
**Context:** `kind-cks`
|
||||||
|
|
||||||
|
**Setup:** none.
|
||||||
|
|
||||||
|
**Task:**
|
||||||
|
1. Create a RuntimeClass `gvisor` with handler `runsc`.
|
||||||
|
2. Create a Pod `sandboxed` in namespace `runtime` (`image: nginx:1.27`) that runs under the `gvisor` RuntimeClass.
|
||||||
|
|
||||||
|
> Note: kind's containerd has no `runsc` handler, so the pod will stay `Pending`/`ContainerCreating` — that's expected. The graded artifact is the correct RuntimeClass + pod spec. (On a VM with gVisor + a containerd `runsc` runtime configured, it would actually run.)
|
||||||
|
|
||||||
|
> Verify: `kubectl get runtimeclass gvisor` exists with handler `runsc`; pod references it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 12 — ValidatingAdmissionPolicy (in-tree, CEL) · Microservice Vuln · 5%
|
||||||
|
|
||||||
|
**Context:** `kind-cks`
|
||||||
|
|
||||||
|
**Setup:** none.
|
||||||
|
|
||||||
|
**Task:**
|
||||||
|
Using the built-in `ValidatingAdmissionPolicy` API (no external webhook, no Kyverno/OPA), block the creation of any Pod that:
|
||||||
|
|
||||||
|
- sets `privileged: true` on any container, **or**
|
||||||
|
- uses a `hostPath` volume.
|
||||||
|
|
||||||
|
Enforce it cluster-wide with `failurePolicy: Fail`. Bind it so it's active.
|
||||||
|
|
||||||
|
> Verify: applying a pod with `privileged: true` or a `hostPath` volume is denied by the policy; a clean pod is admitted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 13 — Trivy image scan & evict vulnerable pods · Supply Chain · 6%
|
||||||
|
|
||||||
|
**Context:** `kind-cks`
|
||||||
|
|
||||||
|
**Setup:** seeded (`images` ns: `legacy-app` [nginx:1.19.0], `old-debian` [debian:10], `clean-app` [nginx:1.27]).
|
||||||
|
|
||||||
|
**Task:**
|
||||||
|
Using `trivy` on the VM, scan the images used by the pods in namespace `images`. **Delete every pod** whose image has at least one **HIGH or CRITICAL** severity, OS-level vulnerability. Leave the rest running.
|
||||||
|
|
||||||
|
> Verify: `legacy-app` and `old-debian` gone; `clean-app` (or whichever scans clean of HIGH/CRITICAL) remains.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 14 — Kyverno policy: no `:latest`, trusted registries only · Supply Chain · 6%
|
||||||
|
|
||||||
|
**Context:** `kind-cks`
|
||||||
|
|
||||||
|
**Setup:** install Kyverno (latest) — do this at the start of the task:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# latest install manifest
|
||||||
|
kubectl create -f https://github.com/kyverno/kyverno/releases/latest/download/install.yaml
|
||||||
|
kubectl -n kyverno rollout status deploy/kyverno-admission-controller --timeout=180s
|
||||||
|
kubectl create ns team-a --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
```
|
||||||
|
|
||||||
|
**Task:**
|
||||||
|
Author Kyverno ClusterPolicies (enforce mode) that:
|
||||||
|
|
||||||
|
1. **Reject** any Pod whose container image uses the `:latest` tag or has no tag.
|
||||||
|
2. **Reject** any Pod whose image does not come from registry `registry.cks.local` (allow that registry only).
|
||||||
|
|
||||||
|
Apply them cluster-wide.
|
||||||
|
|
||||||
|
> Verify: `kubectl -n team-a run bad --image=nginx:latest` → denied; `kubectl -n team-a run bad2 --image=docker.io/nginx:1.27` → denied (wrong registry); `... --image=registry.cks.local/nginx:1.27` → admitted (may fail to pull, but admission passes).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 15 — ImagePolicyWebhook admission control · Supply Chain · 4%
|
||||||
|
|
||||||
|
**Context:** `kind-cks` (control-plane)
|
||||||
|
|
||||||
|
**Setup:** stage the admission config + webhook kubeconfig (you'll write these as part of the task; skeleton paths below):
|
||||||
|
|
||||||
|
- `/etc/kubernetes/admission/admission-config.yaml`
|
||||||
|
- `/etc/kubernetes/admission/imagepolicy-kubeconfig.yaml`
|
||||||
|
|
||||||
|
**Task:**
|
||||||
|
Enable the `ImagePolicyWebhook` admission plugin on the kube-apiserver and wire it to an `AdmissionConfiguration` file that:
|
||||||
|
|
||||||
|
- references the webhook via a kubeconfig,
|
||||||
|
- sets `defaultAllow: false` in the ImagePolicy plugin config,
|
||||||
|
|
||||||
|
Mount both files into the static pod and enable the plugin. (No live backend is required — grading is on the admission plugin being enabled and the config file being correctly referenced. With `defaultAllow: false` and no reachable backend, new pod creation should be **rejected** — demonstrate that, then set `defaultAllow: true` to restore usability if you want the cluster functional for later tasks.)
|
||||||
|
|
||||||
|
> Verify: with `defaultAllow: false`, `kubectl run t --image=nginx:1.27` is rejected by the ImagePolicyWebhook.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 16 — Falco: custom runtime rule · Monitoring/Logging/Runtime · 7%
|
||||||
|
|
||||||
|
**Context:** Ubuntu VM (host-installed Falco) — this mirrors the classic CKS Falco task.
|
||||||
|
|
||||||
|
**Setup:** install Falco on the VM (latest, v0.44.x, modern eBPF):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://falco.org/repo/falcosecurity-packages.asc | sudo gpg --dearmor -o /usr/share/keyrings/falco-archive-keyring.gpg
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] https://download.falco.org/packages/deb stable main" | sudo tee /etc/apt/sources.list.d/falcosecurity.list
|
||||||
|
sudo apt update && sudo apt install -y falco
|
||||||
|
```
|
||||||
|
|
||||||
|
(If you'd rather do it in-cluster: `helm install falco falcosecurity/falco -n falco --create-namespace --set driver.kind=modern_ebpf` — but the on-VM path is closer to the exam.)
|
||||||
|
|
||||||
|
**Task:**
|
||||||
|
1. Add a **custom Falco rule** (in `/etc/falco/falco_rules.local.yaml`) named `Shell in container` that fires at priority `WARNING` whenever a shell (`sh`, `bash`) is spawned inside any container, with output including the container id, container name, and the spawned process name.
|
||||||
|
2. Restart Falco, trigger the rule (spawn a shell in a container), and **capture the alert line** to `/opt/course/falco-hits.txt`.
|
||||||
|
|
||||||
|
> Verify: the rule appears in Falco's loaded rules; triggering a shell produces a `Shell in container` alert containing the container name and proc name.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 17 — API server audit logging · Monitoring/Logging/Runtime · 6%
|
||||||
|
|
||||||
|
**Context:** `kind-cks` (control-plane)
|
||||||
|
|
||||||
|
**Setup:** none.
|
||||||
|
|
||||||
|
**Task:**
|
||||||
|
Configure kube-apiserver audit logging:
|
||||||
|
|
||||||
|
1. Write an audit policy at `/etc/kubernetes/audit/policy.yaml` that:
|
||||||
|
- logs `Metadata` level for `secrets`, `configmaps` in all namespaces,
|
||||||
|
- logs `RequestResponse` level for `pods` changes (create/update/delete),
|
||||||
|
- logs everything else at `None` (don't log read-only noise).
|
||||||
|
2. Enable it on the apiserver: `--audit-policy-file`, `--audit-log-path=/var/log/kubernetes/audit/audit.log`, `--audit-log-maxage=7`, `--audit-log-maxbackup=2`, `--audit-log-maxsize=50`. Mount the policy (read-only) and the log dir (read-write) into the static pod.
|
||||||
|
|
||||||
|
> Verify: after the apiserver restarts, `kubectl -n vault-ns get secret pre-existing` then check `/var/log/kubernetes/audit/audit.log` inside the control-plane contains a metadata-level entry for that secret and no `RequestResponse` bodies for it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scoring yourself
|
||||||
|
- 12+/17 fully correct ≈ comfortable pass.
|
||||||
|
- Time-box: if a control-plane restart task (5, 10, 15, 17) breaks the apiserver, you lose access to *everything* — practice reverting fast (`docker exec ... ` edit back, or `cp` a backup of the manifest you took first). **Always `cp` the manifest before editing.**
|
||||||
|
|
||||||
|
Answers in `answers/solutions.md`. Don't peek until you've timed a full run.
|
||||||
22
kubernetes/cks/exam-by-claude/kind-config.yaml
Normal file
22
kubernetes/cks/exam-by-claude/kind-config.yaml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# CKS mock cluster — 1 control-plane + 2 workers, k8s v1.35
|
||||||
|
# Pinned digest from kind v0.32.0 (default 1.36) — we force 1.35.5 to match the exam.
|
||||||
|
# If `kind load`/image pull complains, use kind >= v0.32.0.
|
||||||
|
kind: Cluster
|
||||||
|
apiVersion: kind.x-k8s.io/v1alpha4
|
||||||
|
name: cks
|
||||||
|
nodes:
|
||||||
|
- role: control-plane
|
||||||
|
image: kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95
|
||||||
|
extraMounts:
|
||||||
|
# host dir for audit policy / encryption config / admission config you'll author on the VM
|
||||||
|
- hostPath: ./exam-files
|
||||||
|
containerPath: /etc/kubernetes/exam
|
||||||
|
extraPortMappings:
|
||||||
|
# so you can curl the Ingress task from the VM
|
||||||
|
- containerPort: 30443
|
||||||
|
hostPort: 30443
|
||||||
|
protocol: TCP
|
||||||
|
- role: worker
|
||||||
|
image: kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95
|
||||||
|
- role: worker
|
||||||
|
image: kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95
|
||||||
162
kubernetes/cks/exam-by-claude/seed.sh
Normal file
162
kubernetes/cks/exam-by-claude/seed.sh
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# CKS mock — seed the cluster with the objects each task expects. Idempotent-ish.
|
||||||
|
set -euo pipefail
|
||||||
|
kubectl config use-context kind-cks >/dev/null
|
||||||
|
|
||||||
|
echo "[*] Namespaces..."
|
||||||
|
for ns in prod web dev apps restricted-ns images sysh runtime; do
|
||||||
|
kubectl create ns "$ns" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------- Task 1: NetworkPolicy ----------
|
||||||
|
kubectl -n prod apply -f - >/dev/null <<'EOF'
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata: {name: db, namespace: prod, labels: {app: db}}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector: {matchLabels: {app: db}}
|
||||||
|
template:
|
||||||
|
metadata: {labels: {app: db}}
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: db
|
||||||
|
image: hashicorp/http-echo:1.0
|
||||||
|
args: ["-text=db-ok", "-listen=:5432"]
|
||||||
|
ports: [{containerPort: 5432}]
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata: {name: db, namespace: prod}
|
||||||
|
spec:
|
||||||
|
selector: {app: db}
|
||||||
|
ports: [{port: 5432, targetPort: 5432}]
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata: {name: frontend, namespace: prod, labels: {app: frontend}}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector: {matchLabels: {app: frontend}}
|
||||||
|
template:
|
||||||
|
metadata: {labels: {app: frontend}}
|
||||||
|
spec: {containers: [{name: c, image: curlimages/curl:8.11.1, command: ["sleep","infinity"]}]}
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata: {name: attacker, namespace: prod, labels: {app: attacker}}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector: {matchLabels: {app: attacker}}
|
||||||
|
template:
|
||||||
|
metadata: {labels: {app: attacker}}
|
||||||
|
spec: {containers: [{name: c, image: curlimages/curl:8.11.1, command: ["sleep","infinity"]}]}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ---------- Task 2: Ingress TLS ----------
|
||||||
|
kubectl -n web apply -f - >/dev/null <<'EOF'
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata: {name: hello, namespace: web}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector: {matchLabels: {app: hello}}
|
||||||
|
template:
|
||||||
|
metadata: {labels: {app: hello}}
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: hello
|
||||||
|
image: hashicorp/http-echo:1.0
|
||||||
|
args: ["-text=hello-tls", "-listen=:5678"]
|
||||||
|
ports: [{containerPort: 5678}]
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata: {name: hello, namespace: web}
|
||||||
|
spec:
|
||||||
|
selector: {app: hello}
|
||||||
|
ports: [{port: 80, targetPort: 5678}]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ---------- Task 4: RBAC least-privilege (over-permissive to fix) ----------
|
||||||
|
kubectl -n dev apply -f - >/dev/null <<'EOF'
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata: {name: ci-runner, namespace: dev}
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata: {name: ci-runner-role, namespace: dev}
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["*"]
|
||||||
|
resources: ["*"]
|
||||||
|
verbs: ["*"] # WAY too broad — task: restrict to get/list/watch on pods & configmaps
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata: {name: ci-runner-rb, namespace: dev}
|
||||||
|
roleRef: {apiGroup: rbac.authorization.k8s.io, kind: Role, name: ci-runner-role}
|
||||||
|
subjects: [{kind: ServiceAccount, name: ci-runner, namespace: dev}]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ---------- Task 6: automountServiceAccountToken ----------
|
||||||
|
kubectl -n apps apply -f - >/dev/null <<'EOF'
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata: {name: web-sa, namespace: apps}
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata: {name: web, namespace: apps}
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
selector: {matchLabels: {app: web}}
|
||||||
|
template:
|
||||||
|
metadata: {labels: {app: web}}
|
||||||
|
spec:
|
||||||
|
serviceAccountName: web-sa
|
||||||
|
containers: [{name: c, image: nginx:1.27, ports: [{containerPort: 80}]}]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ---------- Task 9: Pod Security Admission (violating workload) ----------
|
||||||
|
kubectl -n restricted-ns apply -f - >/dev/null <<'EOF'
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata: {name: payments, namespace: restricted-ns}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector: {matchLabels: {app: payments}}
|
||||||
|
template:
|
||||||
|
metadata: {labels: {app: payments}}
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: c
|
||||||
|
image: nginx:1.27
|
||||||
|
securityContext:
|
||||||
|
privileged: true # violates restricted
|
||||||
|
allowPrivilegeEscalation: true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ---------- Task 13: Trivy image scan (mixed vuln levels) ----------
|
||||||
|
kubectl -n images apply -f - >/dev/null <<'EOF'
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata: {name: legacy-app, namespace: images, labels: {scan: "true"}}
|
||||||
|
spec:
|
||||||
|
containers: [{name: c, image: nginx:1.19.0}] # old, HIGH/CRITICAL CVEs
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata: {name: old-debian, namespace: images, labels: {scan: "true"}}
|
||||||
|
spec:
|
||||||
|
containers: [{name: c, image: debian:10}] # EOL, plenty of CVEs
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata: {name: clean-app, namespace: images, labels: {scan: "true"}}
|
||||||
|
spec:
|
||||||
|
containers: [{name: c, image: nginx:1.27}] # relatively clean
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "[*] Seed complete. Namespaces: prod web dev apps restricted-ns images sysh runtime"
|
||||||
|
echo "[*] Note: Kyverno / Falco / gVisor get installed inside their own task setup blocks."
|
||||||
545
kubernetes/cks/exam-by-claude/solutions.md
Normal file
545
kubernetes/cks/exam-by-claude/solutions.md
Normal file
@@ -0,0 +1,545 @@
|
|||||||
|
# CKS Mock — Solutions (k8s v1.35)
|
||||||
|
|
||||||
|
Reference answers. There's usually more than one valid path; these are the exam-fast ones. Control-plane file edits assume `docker exec -it cks-control-plane bash` (or `cks-worker` for node tasks).
|
||||||
|
|
||||||
|
> **Before ANY apiserver edit:** `cp /etc/kubernetes/manifests/kube-apiserver.yaml ~/kube-apiserver.yaml.bak` inside the node. If the pod won't come back, `cp` it back.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1 — NetworkPolicy
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata: {name: default-deny, namespace: prod}
|
||||||
|
spec:
|
||||||
|
podSelector: {}
|
||||||
|
policyTypes: [Ingress]
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata: {name: allow-frontend, namespace: prod}
|
||||||
|
spec:
|
||||||
|
podSelector: {matchLabels: {app: db}}
|
||||||
|
policyTypes: [Ingress]
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- podSelector: {matchLabels: {app: frontend}}
|
||||||
|
ports:
|
||||||
|
- {protocol: TCP, port: 5432}
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
```bash
|
||||||
|
kubectl -n prod exec deploy/frontend -- curl -s --max-time 3 db:5432 # -> db-ok
|
||||||
|
kubectl -n prod exec deploy/attacker -- curl -s --max-time 3 db:5432 # -> timeout
|
||||||
|
```
|
||||||
|
Gotcha: kind's default CNI (kindnetd) enforces NetworkPolicy in recent versions; if your kindnet build doesn't, swap to Calico. On the exam CNI always enforces.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2 — Ingress TLS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||||
|
-keyout tls.key -out tls.crt -subj "/CN=hello.cks.local/O=cks"
|
||||||
|
kubectl -n web create secret tls hello-tls --cert=tls.crt --key=tls.key
|
||||||
|
```
|
||||||
|
```yaml
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: hello
|
||||||
|
namespace: web
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
tls:
|
||||||
|
- hosts: [hello.cks.local]
|
||||||
|
secretName: hello-tls
|
||||||
|
rules:
|
||||||
|
- host: hello.cks.local
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend: {service: {name: hello, port: {number: 80}}}
|
||||||
|
```
|
||||||
|
Verify (port depends on how ingress-nginx is exposed in kind — NodePort or the hostPort mapping):
|
||||||
|
```bash
|
||||||
|
kubectl -n ingress-nginx get svc ingress-nginx-controller
|
||||||
|
curl -k --resolve hello.cks.local:443:127.0.0.1 https://hello.cks.local/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3 — kube-bench remediation
|
||||||
|
|
||||||
|
Run it (Job is easiest inside kind):
|
||||||
|
```bash
|
||||||
|
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
|
||||||
|
kubectl logs -f job/kube-bench | less
|
||||||
|
```
|
||||||
|
Or download the binary and `kube-bench run --targets master,node` on the node.
|
||||||
|
|
||||||
|
Kubelet — edit on the node `/var/lib/kubelet/config.yaml`:
|
||||||
|
```yaml
|
||||||
|
authentication:
|
||||||
|
anonymous:
|
||||||
|
enabled: false
|
||||||
|
webhook:
|
||||||
|
enabled: true
|
||||||
|
authorization:
|
||||||
|
mode: Webhook
|
||||||
|
readOnlyPort: 0
|
||||||
|
```
|
||||||
|
```bash
|
||||||
|
systemctl restart kubelet
|
||||||
|
```
|
||||||
|
API server — `/etc/kubernetes/manifests/kube-apiserver.yaml`, add to `command`:
|
||||||
|
```
|
||||||
|
- --profiling=false
|
||||||
|
```
|
||||||
|
Re-run kube-bench; those checks flip to PASS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4 — RBAC least privilege
|
||||||
|
|
||||||
|
Replace the Role (binding already points at `ci-runner-role`):
|
||||||
|
```yaml
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata: {name: ci-runner-role, namespace: dev}
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["pods", "configmaps"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
```
|
||||||
|
```bash
|
||||||
|
kubectl -n dev replace -f role.yaml # or edit
|
||||||
|
kubectl auth can-i get pods --as=system:serviceaccount:dev:ci-runner -n dev # yes
|
||||||
|
kubectl auth can-i delete pods --as=system:serviceaccount:dev:ci-runner -n dev # no
|
||||||
|
kubectl auth can-i '*' secrets --as=system:serviceaccount:dev:ci-runner -n dev # no
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5 — API server hardening
|
||||||
|
|
||||||
|
`/etc/kubernetes/manifests/kube-apiserver.yaml`, in `command`:
|
||||||
|
```
|
||||||
|
- --anonymous-auth=false
|
||||||
|
- --profiling=false
|
||||||
|
- --enable-admission-plugins=NodeRestriction # merge with existing list, comma-separated
|
||||||
|
```
|
||||||
|
If `--enable-admission-plugins` already exists, append `,NodeRestriction` to it — don't add a second flag.
|
||||||
|
Wait for restart:
|
||||||
|
```bash
|
||||||
|
kubectl -n kube-system get pod -l component=kube-apiserver -w
|
||||||
|
```
|
||||||
|
Note: with `--anonymous-auth=false`, `/healthz` and `/livez` still work for the kubelet because it authenticates; unauthenticated curl gets 401.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6 — automountServiceAccountToken
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n apps patch sa web-sa -p '{"automountServiceAccountToken": false}'
|
||||||
|
kubectl -n apps rollout restart deploy/web
|
||||||
|
kubectl -n apps exec deploy/web -- ls /var/run/secrets/kubernetes.io/serviceaccount 2>&1 # No such file
|
||||||
|
```
|
||||||
|
(Alternatively set `automountServiceAccountToken: false` in the pod template — SA-level is cleaner here and is what the task asks.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7 — AppArmor (GA securityContext API)
|
||||||
|
|
||||||
|
Load profile on the worker:
|
||||||
|
```bash
|
||||||
|
docker exec cks-worker apparmor_parser -q /root/deny-write
|
||||||
|
docker exec cks-worker aa-status | grep k8s-deny-write # enforce
|
||||||
|
```
|
||||||
|
Pod — **use the appArmorProfile field, not the beta annotation**:
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata: {name: locked, namespace: sysh}
|
||||||
|
spec:
|
||||||
|
nodeName: cks-worker
|
||||||
|
securityContext:
|
||||||
|
appArmorProfile:
|
||||||
|
type: Localhost
|
||||||
|
localhostProfile: k8s-deny-write
|
||||||
|
containers:
|
||||||
|
- name: c
|
||||||
|
image: busybox:1.36
|
||||||
|
command: ["sh","-c","sleep 1h"]
|
||||||
|
```
|
||||||
|
```bash
|
||||||
|
kubectl -n sysh exec locked -- sh -c 'echo x > /tmp/x' # Permission denied
|
||||||
|
```
|
||||||
|
Gotcha: the profile only exists on `cks-worker`, so you must pin the pod there or the load must be on every schedulable node.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 8 — seccomp
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata: {name: traced, namespace: sysh}
|
||||||
|
spec:
|
||||||
|
nodeName: cks-worker
|
||||||
|
securityContext:
|
||||||
|
seccompProfile:
|
||||||
|
type: Localhost
|
||||||
|
localhostProfile: profiles/audit.json
|
||||||
|
containers:
|
||||||
|
- {name: c, image: busybox:1.36, command: ["sh","-c","sleep 1h"]}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata: {name: defaulted, namespace: sysh}
|
||||||
|
spec:
|
||||||
|
nodeName: cks-worker
|
||||||
|
securityContext:
|
||||||
|
seccompProfile: {type: RuntimeDefault}
|
||||||
|
containers:
|
||||||
|
- {name: c, image: busybox:1.36, command: ["sh","-c","sleep 1h"]}
|
||||||
|
```
|
||||||
|
`localhostProfile` is relative to the kubelet seccomp root (`/var/lib/kubelet/seccomp`), so `profiles/audit.json` resolves to `/var/lib/kubelet/seccomp/profiles/audit.json`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 9 — Pod Security Admission (restricted)
|
||||||
|
|
||||||
|
Label the namespace:
|
||||||
|
```bash
|
||||||
|
kubectl label ns restricted-ns \
|
||||||
|
pod-security.kubernetes.io/enforce=restricted \
|
||||||
|
pod-security.kubernetes.io/enforce-version=latest \
|
||||||
|
pod-security.kubernetes.io/warn=restricted \
|
||||||
|
pod-security.kubernetes.io/audit=restricted --overwrite
|
||||||
|
```
|
||||||
|
Fix the Deployment pod template to satisfy `restricted`:
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
seccompProfile: {type: RuntimeDefault}
|
||||||
|
containers:
|
||||||
|
- name: c
|
||||||
|
image: nginx:1.27
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
privileged: false
|
||||||
|
runAsNonRoot: true
|
||||||
|
capabilities: {drop: ["ALL"]}
|
||||||
|
```
|
||||||
|
nginx:1.27 wants to bind :80 — under `runAsNonRoot` it'll fail unless you use an unprivileged image/port. For the exam-grade "complies + rolls out," use `nginxinc/nginx-unprivileged:1.27` (listens on 8080) or set `runAsUser: 101`. The graded bit is restricted-compliance; the unprivileged image makes it actually run.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 10 — Encryption at rest
|
||||||
|
|
||||||
|
Key + config on the control-plane:
|
||||||
|
```bash
|
||||||
|
mkdir -p /etc/kubernetes/enc
|
||||||
|
head -c 32 /dev/urandom | base64 # copy the value
|
||||||
|
cat > /etc/kubernetes/enc/enc.yaml <<EOF
|
||||||
|
apiVersion: apiserver.config.k8s.io/v1
|
||||||
|
kind: EncryptionConfiguration
|
||||||
|
resources:
|
||||||
|
- resources: ["secrets"]
|
||||||
|
providers:
|
||||||
|
- aescbc:
|
||||||
|
keys:
|
||||||
|
- name: key1
|
||||||
|
secret: <PASTE_BASE64_32B_KEY>
|
||||||
|
- identity: {}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
apiserver manifest — add flag + volume + mount:
|
||||||
|
```yaml
|
||||||
|
# command:
|
||||||
|
- --encryption-provider-config=/etc/kubernetes/enc/enc.yaml
|
||||||
|
# volumes:
|
||||||
|
- name: enc
|
||||||
|
hostPath: {path: /etc/kubernetes/enc, type: DirectoryOrCreate}
|
||||||
|
# volumeMounts:
|
||||||
|
- name: enc
|
||||||
|
mountPath: /etc/kubernetes/enc
|
||||||
|
readOnly: true
|
||||||
|
```
|
||||||
|
After apiserver is back, rewrite all secrets:
|
||||||
|
```bash
|
||||||
|
kubectl get secrets -A -o json | kubectl replace -f -
|
||||||
|
```
|
||||||
|
Verify with etcdctl inside the control-plane:
|
||||||
|
```bash
|
||||||
|
ETCDCTL_API=3 etcdctl \
|
||||||
|
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
|
||||||
|
--cert=/etc/kubernetes/pki/etcd/server.crt \
|
||||||
|
--key=/etc/kubernetes/pki/etcd/server.key \
|
||||||
|
get /registry/secrets/vault-ns/pre-existing | hexdump -C | head
|
||||||
|
# expect: k8s:enc:aescbc:v1:key1:... and NO plaintext SUPERSECRET
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 11 — RuntimeClass
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: node.k8s.io/v1
|
||||||
|
kind: RuntimeClass
|
||||||
|
metadata: {name: gvisor}
|
||||||
|
handler: runsc
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata: {name: sandboxed, namespace: runtime}
|
||||||
|
spec:
|
||||||
|
runtimeClassName: gvisor
|
||||||
|
containers:
|
||||||
|
- {name: c, image: nginx:1.27}
|
||||||
|
```
|
||||||
|
Stays Pending in kind (no `runsc` handler in containerd) — expected. Config is the deliverable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 12 — ValidatingAdmissionPolicy
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: admissionregistration.k8s.io/v1
|
||||||
|
kind: ValidatingAdmissionPolicy
|
||||||
|
metadata: {name: pod-hardening}
|
||||||
|
spec:
|
||||||
|
failurePolicy: Fail
|
||||||
|
matchConstraints:
|
||||||
|
resourceRules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
apiVersions: ["v1"]
|
||||||
|
operations: ["CREATE","UPDATE"]
|
||||||
|
resources: ["pods"]
|
||||||
|
validations:
|
||||||
|
- expression: >-
|
||||||
|
!object.spec.containers.exists(c,
|
||||||
|
has(c.securityContext) && has(c.securityContext.privileged)
|
||||||
|
&& c.securityContext.privileged == true)
|
||||||
|
message: "privileged containers are not allowed"
|
||||||
|
- expression: >-
|
||||||
|
!has(object.spec.volumes) ||
|
||||||
|
!object.spec.volumes.exists(v, has(v.hostPath))
|
||||||
|
message: "hostPath volumes are not allowed"
|
||||||
|
---
|
||||||
|
apiVersion: admissionregistration.k8s.io/v1
|
||||||
|
kind: ValidatingAdmissionPolicyBinding
|
||||||
|
metadata: {name: pod-hardening-binding}
|
||||||
|
spec:
|
||||||
|
policyName: pod-hardening
|
||||||
|
validationActions: ["Deny"]
|
||||||
|
matchResources: {} # all namespaces
|
||||||
|
```
|
||||||
|
Also check `initContainers`/`ephemeralContainers` if you want it airtight; for the task, `containers` is what's graded. Test:
|
||||||
|
```bash
|
||||||
|
kubectl run bad --image=nginx:1.27 --privileged # denied
|
||||||
|
kubectl run ok --image=nginx:1.27 # admitted
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 13 — Trivy scan & evict
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for img in nginx:1.19.0 debian:10 nginx:1.27; do
|
||||||
|
echo "== $img =="
|
||||||
|
trivy image --severity HIGH,CRITICAL --quiet --scanners vuln "$img" | tail -5
|
||||||
|
done
|
||||||
|
```
|
||||||
|
`nginx:1.19.0` and `debian:10` will show HIGH/CRITICAL; delete those pods:
|
||||||
|
```bash
|
||||||
|
kubectl -n images delete pod legacy-app old-debian
|
||||||
|
kubectl -n images get pods # clean-app remains
|
||||||
|
```
|
||||||
|
Exam tip: map image→pod first (`kubectl -n images get pods -o custom-columns=POD:.metadata.name,IMG:.spec.containers[*].image`), scan each, delete by HIGH/CRITICAL count > 0.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 14 — Kyverno policies
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: kyverno.io/v1
|
||||||
|
kind: ClusterPolicy
|
||||||
|
metadata: {name: disallow-latest-tag}
|
||||||
|
spec:
|
||||||
|
validationFailureAction: Enforce
|
||||||
|
background: false
|
||||||
|
rules:
|
||||||
|
- name: require-explicit-tag
|
||||||
|
match: {any: [{resources: {kinds: [Pod]}}]}
|
||||||
|
validate:
|
||||||
|
message: "images must not use :latest or an empty tag"
|
||||||
|
pattern:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: "!*:latest"
|
||||||
|
- name: require-tag-present
|
||||||
|
match: {any: [{resources: {kinds: [Pod]}}]}
|
||||||
|
validate:
|
||||||
|
message: "image tag is required"
|
||||||
|
pattern:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: "*:*"
|
||||||
|
---
|
||||||
|
apiVersion: kyverno.io/v1
|
||||||
|
kind: ClusterPolicy
|
||||||
|
metadata: {name: allowed-registries}
|
||||||
|
spec:
|
||||||
|
validationFailureAction: Enforce
|
||||||
|
background: false
|
||||||
|
rules:
|
||||||
|
- name: only-cks-registry
|
||||||
|
match: {any: [{resources: {kinds: [Pod]}}]}
|
||||||
|
validate:
|
||||||
|
message: "images must come from registry.cks.local"
|
||||||
|
pattern:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: "registry.cks.local/*"
|
||||||
|
```
|
||||||
|
Test:
|
||||||
|
```bash
|
||||||
|
kubectl -n team-a run bad --image=nginx:latest # denied (latest)
|
||||||
|
kubectl -n team-a run bad2 --image=docker.io/nginx:1.27 # denied (registry)
|
||||||
|
kubectl -n team-a run good --image=registry.cks.local/nginx:1.27 # admitted
|
||||||
|
```
|
||||||
|
Note: recent Kyverno also exposes the newer `validate.foreach`/CEL syntax; the pattern form above is the fastest to write under time.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 15 — ImagePolicyWebhook
|
||||||
|
|
||||||
|
`/etc/kubernetes/admission/admission-config.yaml`:
|
||||||
|
```yaml
|
||||||
|
apiVersion: apiserver.config.k8s.io/v1
|
||||||
|
kind: AdmissionConfiguration
|
||||||
|
plugins:
|
||||||
|
- name: ImagePolicyWebhook
|
||||||
|
configuration:
|
||||||
|
imagePolicy:
|
||||||
|
kubeConfigFile: /etc/kubernetes/admission/imagepolicy-kubeconfig.yaml
|
||||||
|
allowTTL: 50
|
||||||
|
denyTTL: 50
|
||||||
|
retryBackoff: 500
|
||||||
|
defaultAllow: false
|
||||||
|
```
|
||||||
|
`/etc/kubernetes/admission/imagepolicy-kubeconfig.yaml`:
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Config
|
||||||
|
clusters:
|
||||||
|
- name: image-checker
|
||||||
|
cluster:
|
||||||
|
server: https://image-checker.local/check # no live backend needed for the drill
|
||||||
|
users:
|
||||||
|
- name: apiserver
|
||||||
|
contexts:
|
||||||
|
- name: default
|
||||||
|
context: {cluster: image-checker, user: apiserver}
|
||||||
|
current-context: default
|
||||||
|
```
|
||||||
|
apiserver manifest:
|
||||||
|
```yaml
|
||||||
|
# command:
|
||||||
|
- --enable-admission-plugins=NodeRestriction,ImagePolicyWebhook # merge with existing
|
||||||
|
- --admission-control-config-file=/etc/kubernetes/admission/admission-config.yaml
|
||||||
|
# volume + mount for /etc/kubernetes/admission (readOnly)
|
||||||
|
```
|
||||||
|
With `defaultAllow: false` and no reachable backend, `kubectl run t --image=nginx:1.27` → rejected by ImagePolicyWebhook. Flip to `defaultAllow: true` afterwards if you want the cluster usable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 16 — Falco custom rule
|
||||||
|
|
||||||
|
`/etc/falco/falco_rules.local.yaml`:
|
||||||
|
```yaml
|
||||||
|
- rule: Shell in container
|
||||||
|
desc: Detect a shell spawned inside a container
|
||||||
|
condition: >
|
||||||
|
spawned_process and container
|
||||||
|
and proc.name in (sh, bash)
|
||||||
|
output: >
|
||||||
|
Shell in container (container_id=%container.id container_name=%container.name
|
||||||
|
proc=%proc.name user=%user.name)
|
||||||
|
priority: WARNING
|
||||||
|
tags: [container, shell, mitre_execution]
|
||||||
|
```
|
||||||
|
```bash
|
||||||
|
systemctl restart falco # or: falco -r /etc/falco/falco_rules.yaml -r /etc/falco/falco_rules.local.yaml
|
||||||
|
# trigger:
|
||||||
|
kubectl run trigger --image=busybox:1.36 -- sh -c 'sleep 1h'
|
||||||
|
kubectl exec -it trigger -- sh
|
||||||
|
# capture:
|
||||||
|
journalctl -u falco | grep "Shell in container" | tail -1 > /opt/course/falco-hits.txt
|
||||||
|
```
|
||||||
|
Gotchas: `container` macro excludes host processes; `spawned_process` = execve. If running Falco via systemd, alerts land in `journalctl -u falco` (and/or `/var/log/syslog`) — grab from whichever your output channel is. Check the rule loaded: `falco --list | grep -i "Shell in container"` or watch startup logs for parse errors.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 17 — Audit logging
|
||||||
|
|
||||||
|
`/etc/kubernetes/audit/policy.yaml`:
|
||||||
|
```yaml
|
||||||
|
apiVersion: audit.k8s.io/v1
|
||||||
|
kind: Policy
|
||||||
|
omitStages: ["RequestReceived"]
|
||||||
|
rules:
|
||||||
|
- level: RequestResponse
|
||||||
|
resources:
|
||||||
|
- group: ""
|
||||||
|
resources: ["pods"]
|
||||||
|
verbs: ["create","update","patch","delete"]
|
||||||
|
- level: Metadata
|
||||||
|
resources:
|
||||||
|
- group: ""
|
||||||
|
resources: ["secrets","configmaps"]
|
||||||
|
- level: None
|
||||||
|
```
|
||||||
|
apiserver manifest — flags:
|
||||||
|
```yaml
|
||||||
|
- --audit-policy-file=/etc/kubernetes/audit/policy.yaml
|
||||||
|
- --audit-log-path=/var/log/kubernetes/audit/audit.log
|
||||||
|
- --audit-log-maxage=7
|
||||||
|
- --audit-log-maxbackup=2
|
||||||
|
- --audit-log-maxsize=50
|
||||||
|
```
|
||||||
|
volumes + mounts:
|
||||||
|
```yaml
|
||||||
|
# volumes:
|
||||||
|
- name: audit-policy
|
||||||
|
hostPath: {path: /etc/kubernetes/audit, type: DirectoryOrCreate}
|
||||||
|
- name: audit-logs
|
||||||
|
hostPath: {path: /var/log/kubernetes/audit, type: DirectoryOrCreate}
|
||||||
|
# volumeMounts:
|
||||||
|
- name: audit-policy
|
||||||
|
mountPath: /etc/kubernetes/audit
|
||||||
|
readOnly: true
|
||||||
|
- name: audit-logs
|
||||||
|
mountPath: /var/log/kubernetes/audit
|
||||||
|
readOnly: false
|
||||||
|
```
|
||||||
|
Verify inside the control-plane:
|
||||||
|
```bash
|
||||||
|
kubectl -n vault-ns get secret pre-existing
|
||||||
|
tail -f /var/log/kubernetes/audit/audit.log | grep pre-existing # Metadata level, no body
|
||||||
|
```
|
||||||
|
Big gotcha: the audit-logs mount must be `readOnly: false` or the apiserver crashloops silently. And the log dir hostPath is *inside the control-plane container* — that's fine here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Reset between attempts
|
||||||
|
```bash
|
||||||
|
kind delete cluster --name cks
|
||||||
|
./bootstrap.sh && ./seed.sh
|
||||||
|
```
|
||||||
403
kubernetes/cks/kodekloud-mock-1.md
Normal file
403
kubernetes/cks/kodekloud-mock-1.md
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
# Task 1
|
||||||
|
|
||||||
|
A pod has been created in the omni namespace, but it has a few issues that need to be addressed.
|
||||||
|
|
||||||
|
The pod has been created with more permissions than it needs.
|
||||||
|
It allows read access to the /usr/share/nginx/html/internal directory, making the Internal Site publicly accessible.
|
||||||
|
To verify this, click the Site button (above the terminal) and add /internal/ to the end of the URL.
|
||||||
|
|
||||||
|
Use the below recommendations to resolve this.
|
||||||
|
|
||||||
|
|
||||||
|
Use the AppArmor profile created at /etc/apparmor.d/frontend to restrict access to the internal site.
|
||||||
|
The omni namespace has several service accounts. Apply the principle of least privilege and use the service account with the minimum privileges (excluding the default service account).
|
||||||
|
Once the pod is recreated with the correct service account, delete the other unused service accounts in the omni namespace (excluding the default service account).
|
||||||
|
Do not create a new service account or use the default service account.
|
||||||
|
|
||||||
|
Is the correct service account used?
|
||||||
|
Are obsolete service accounts deleted?
|
||||||
|
Is the internal-site restricted?
|
||||||
|
Is the pod running?
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apparmor_parser /etc/apparmor.d/frontend
|
||||||
|
apparmor_status | grep frontend
|
||||||
|
|
||||||
|
cat p.yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
generation: 1
|
||||||
|
labels:
|
||||||
|
run: nginx
|
||||||
|
name: frontend-site
|
||||||
|
namespace: omni
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: nginx:alpine
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
name: nginx
|
||||||
|
resources: {}
|
||||||
|
terminationMessagePath: /dev/termination-log
|
||||||
|
terminationMessagePolicy: File
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /usr/share/nginx/html
|
||||||
|
name: test-volume
|
||||||
|
dnsPolicy: ClusterFirst
|
||||||
|
enableServiceLinks: true
|
||||||
|
nodeName: controlplane
|
||||||
|
preemptionPolicy: PreemptLowerPriority
|
||||||
|
priority: 0
|
||||||
|
restartPolicy: Always
|
||||||
|
schedulerName: default-scheduler
|
||||||
|
securityContext:
|
||||||
|
appArmorProfile:
|
||||||
|
type: Localhost
|
||||||
|
localhostProfile: restricted-frontend
|
||||||
|
serviceAccount: frontend-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:
|
||||||
|
- hostPath:
|
||||||
|
path: /data/pages
|
||||||
|
type: Directory
|
||||||
|
name: test-volume
|
||||||
|
- name: kube-api-access-z88t9
|
||||||
|
projected:
|
||||||
|
defaultMode: 420
|
||||||
|
sources:
|
||||||
|
- configMap:
|
||||||
|
items:
|
||||||
|
- key: ca.crt
|
||||||
|
path: ca.crt
|
||||||
|
name: kube-root-ca.crt
|
||||||
|
- downwardAPI:
|
||||||
|
items:
|
||||||
|
- fieldRef:
|
||||||
|
apiVersion: v1
|
||||||
|
fieldPath: metadata.namespace
|
||||||
|
path: namespace
|
||||||
|
|
||||||
|
k auth can-i --list --as=system:serviceaccount:omni:frontend-default
|
||||||
|
```
|
||||||
|
|
||||||
|
# Task 2
|
||||||
|
|
||||||
|
A pod has been created in the orion namespace. It uses secrets as environment variables. Extract the decoded secret for the CONNECTOR_PASSWORD and place it under /root/CKS/secrets/CONNECTOR_PASSWORD.
|
||||||
|
|
||||||
|
You are not yet done; instead of using secrets as an environment variable, mount the secret as a read-only volume at the path /mnt/connector/password, which the application can then use.
|
||||||
|
|
||||||
|
Is the pod secured?
|
||||||
|
Is the secret mounted as read-only?
|
||||||
|
Is the existing secret extracted to file?
|
||||||
|
|
||||||
|
# Task 3
|
||||||
|
|
||||||
|
The Release Engineering Team has shared some YAML manifests and Dockerfiles with you for review. These files are located under /opt/course/.
|
||||||
|
|
||||||
|
As a container security expert, your task is to perform a manual static analysis and identify possible security issues related to unwanted credential exposure. Note that running processes as root is not a concern in this task.
|
||||||
|
|
||||||
|
Record the filenames containing issues in /opt/course/security-issues.txt.
|
||||||
|
|
||||||
|
Note: Assume that all referenced files, folders, secrets, and volume mounts are present in the Dockerfiles and YAML manifests. You can ignore any syntax or logic errors.
|
||||||
|
|
||||||
|
Are the filenames containing issues written in /opt/course/security-issues.txt?
|
||||||
|
|
||||||
|
# Task 4
|
||||||
|
|
||||||
|
Create a new pod named audit-nginx in the default namespace using the nginx:alpine image. Secure the syscalls that this pod can use by using the audit.json seccomp profile in the pod's security context.
|
||||||
|
|
||||||
|
|
||||||
|
The audit.json file is provided at the /root/CKS directory. Move it into the profiles directory inside the default seccomp directory before creating the pod.
|
||||||
|
|
||||||
|
Does audit-nginx use the right image?
|
||||||
|
Is the pod running?
|
||||||
|
Does the pod use the correct seccomp profile?
|
||||||
|
|
||||||
|
# Task 5
|
||||||
|
|
||||||
|
We have identified a few issues with our kubernetes setup and need your help in fixing them.
|
||||||
|
|
||||||
|
Fix the following issues on kubelet:
|
||||||
|
|
||||||
|
Kubelet service file permission issues
|
||||||
|
Kubelet config.yaml permission issues
|
||||||
|
Fix the following issues on etcd:
|
||||||
|
|
||||||
|
Incorrect ownership of the etcd directory
|
||||||
|
Fix the following issues on the controlplane node:
|
||||||
|
|
||||||
|
Incorrect value of the profiling argument for:
|
||||||
|
kube-controller-manager
|
||||||
|
kube-scheduler
|
||||||
|
Kube-bench is installed, and its config files are available under /opt/kube-bench. Use the cis-1.10 benchmark with the current Kubernetes version.
|
||||||
|
|
||||||
|
Note: Only fix issues that have the status FAIL, except issue number 1.2.5. Also, ignore the issues with policies.
|
||||||
|
|
||||||
|
Is the apiserver working?
|
||||||
|
Are the issues fixed for the controller and scheduler on the controlplane node?
|
||||||
|
Is the issue pertaining to the etcd data directory fixed?
|
||||||
|
Is the kubelet service file issue fixed?
|
||||||
|
Is the kubelet config file issue fixed?
|
||||||
|
|
||||||
|
# Task 6
|
||||||
|
|
||||||
|
There is suspicious activity in the cluster involving one of the pods running the httpd:2.4-alpine image.
|
||||||
|
|
||||||
|
Falco generates frequent alerts that start with: File below a known binary directory opened for writing.
|
||||||
|
|
||||||
|
Identify the rule causing this alert and update it as per the below requirements:
|
||||||
|
|
||||||
|
Set the rule priority to CRITICAL (Note: Falco will format this as "Critical" in log output)
|
||||||
|
|
||||||
|
The rule output must be:
|
||||||
|
File below a known binary directory opened for writing (user_id=%user.uid file_updated=%fd.name command=%proc.cmdline)
|
||||||
|
|
||||||
|
Configure alerts to be logged to: /opt/security_incidents/alerts.log.
|
||||||
|
|
||||||
|
|
||||||
|
Do not update the default rules file directly. Instead, use the falco_rules.local.yaml file to override.
|
||||||
|
|
||||||
|
Expected log format:
|
||||||
|
|
||||||
|
<timestamp>: Critical File below a known binary directory opened for writing (user_id=0 file_updated=/bin/sleep command=tar -xmf - -C /bin)
|
||||||
|
|
||||||
|
Note: After updating the alert rule, it may take up to a minute for the alerts to appear in the new log location.
|
||||||
|
|
||||||
|
# Task 7
|
||||||
|
|
||||||
|
A deployment named fruits in the namespace salad has three containers:
|
||||||
|
|
||||||
|
apple
|
||||||
|
banana, and
|
||||||
|
kiwi
|
||||||
|
One of these containers has the package curl installed. Identify which container has that package from the running containers, and create an SBOM SPDX for the container's image.
|
||||||
|
|
||||||
|
Use the tarball archive of that particular image stored under /root/ImageTarballs directory for generating the SPDX JSON. The archives have names matching their images.
|
||||||
|
|
||||||
|
Save the output in ~/bugged-fruit.spdx. Save the container name in ~/bugged-container.txt.
|
||||||
|
|
||||||
|
Note: bom as well as all its required dependencies have already been installed.
|
||||||
|
|
||||||
|
Is the SPDX JSON SBOM for the image with curl installed stored at ~/bugged-fruit.spdx?
|
||||||
|
Is the container name stored in ~/bugged-container.txt?
|
||||||
|
|
||||||
|
# Task 8
|
||||||
|
|
||||||
|
Create a service account named bot-sa in the namespace automated. Make sure that this service account does not get automatically mounted to workloads.
|
||||||
|
|
||||||
|
A workload named sweeper is also in the automated namespace. Set the deployment's service account to the newly created service account, and mount the service account token as a projected volume. Do not change any other fields in the deployment.
|
||||||
|
|
||||||
|
Has the service account bot-sa been created without auto-mount?
|
||||||
|
Has the bot-sa service account been set as the deployment's service account?
|
||||||
|
Has the bot-sa service account token been mounted to the deployment as a projected volume?
|
||||||
|
No default auto-mount on the Pod ?
|
||||||
|
|
||||||
|
# Task 9
|
||||||
|
A deployment named web-server is running in the restricted namespace.
|
||||||
|
|
||||||
|
Identify the reason why the deployment is not in a running state; fix the issue so that it can be in a running state.
|
||||||
|
|
||||||
|
Do not change the namespace labels or container image.
|
||||||
|
|
||||||
|
Has the deployment been fixed, and is the pod in a running state?
|
||||||
|
Namespace still enforces restricted ?
|
||||||
|
|
||||||
|
|
||||||
|
# Task 10
|
||||||
|
Deployment web-app is running in the products namespace.
|
||||||
|
|
||||||
|
Database product-db is running in the database namespace.
|
||||||
|
|
||||||
|
Create a network policy named allow-traffic-to-products that allows traffic from product-db to the web-app workload, as well as all traffic originating from the payments namespace.
|
||||||
|
|
||||||
|
Utilize the labels applied on the relevant resources.
|
||||||
|
|
||||||
|
Has the network policy allow-traffic-to-products been created?
|
||||||
|
Is traffic allowed from product-db to web-app?
|
||||||
|
Is traffic allowed from payments-app to web-app?
|
||||||
|
Is traffic not allowed from a test app in the database namespace?
|
||||||
|
Is traffic not allowed from a test app in the default namespace?
|
||||||
|
|
||||||
|
# Task 11
|
||||||
|
|
||||||
|
Edit the gamma deployment in the galaxy namespace to ensure that all containers meet the following requirements:
|
||||||
|
|
||||||
|
Run as user 1001
|
||||||
|
Do not allow privilege escalation
|
||||||
|
Mount their file systems as read-only
|
||||||
|
|
||||||
|
Is the deployment edited to enable both containers to run as user 1001?
|
||||||
|
Is the deployment edited to deny privilege escalation on both containers?
|
||||||
|
Is the deployment edited to mount read-only file systems on both containers?
|
||||||
|
|
||||||
|
# Task 12
|
||||||
|
|
||||||
|
A deployment rocket-server is exposed using the service of the same name in the space namespace.
|
||||||
|
|
||||||
|
Create an ingress resource named rocket-ingress to load balance the incoming traffic to the workload on path /.
|
||||||
|
|
||||||
|
Use the hostname rocket-server.local in the Ingress rules.
|
||||||
|
|
||||||
|
Utilize the TLS certificate stored in a secret named rocket-tls in the space namespace so that it enables TLS traffic on that ingress resource.
|
||||||
|
|
||||||
|
Is the ingress resource correctly created?
|
||||||
|
Has the ingress TLS secret been attached?
|
||||||
|
Does the ingress route to the correct path and port?
|
||||||
|
Is curl working for the HTTPS connection via the ingress?
|
||||||
|
|
||||||
|
# Task 13
|
||||||
|
|
||||||
|
A developer named martin needs access to work on the dev-a, dev-b, and dev-z namespaces. He should have the ability to carry out any operation on any pod in the dev-a and dev-b namespaces. However, on the dev-z namespace, he should only have the permission to get and list the pods.
|
||||||
|
|
||||||
|
|
||||||
|
The current setup is too permissive and violates the above condition. Use the above requirement and secure martin's access in the cluster. You may re-create objects; however, ensure to use the same names as the ones in currently in effect.
|
||||||
|
|
||||||
|
Does martin have unrestricted access to all pods in dev-a?
|
||||||
|
Does martin have unrestricted access to all pods in dev-b?
|
||||||
|
Can martin only list and get pods in dev-z?
|
||||||
|
|
||||||
|
|
||||||
|
# Task 14
|
||||||
|
|
||||||
|
You need to enable auditing on this cluster. A basic policy file is available at /etc/kubernetes/cluster-policy.yaml.
|
||||||
|
|
||||||
|
The logs should be stored at /var/log/cluster-audit.log. The logs should be retained for 10 days and should not exceed 10MB. A maximum of 3 files should be kept at a time.
|
||||||
|
|
||||||
|
After you enable auditing on the cluster, update the basic policy file to track the following:
|
||||||
|
|
||||||
|
Delete activity on secrets in the kube-system namespace at the Metadata level
|
||||||
|
Changes to deployments in the default namespace at the Request level
|
||||||
|
All other requests at the Metadata level
|
||||||
|
Make sure your changes to the policy file are in effect.
|
||||||
|
|
||||||
|
Note: A copy of the kube-apiserver.yaml is kept in ~/ so that you can revert if the configuration goes wrong. Make sure kube-apiserver is working fine for the sake of grading the exam.
|
||||||
|
|
||||||
|
|
||||||
|
Does /var/log/cluster-audit.log exist with the logs written to it?
|
||||||
|
|
||||||
|
Does auditing follow the mentioned parameters as specified in the question?
|
||||||
|
|
||||||
|
Is the delete activity on secrets tracked at the Metadata level in the kube-system namespace?
|
||||||
|
|
||||||
|
Is the edit activity on deployments tracked at the Request level in the default namespace?
|
||||||
|
|
||||||
|
# Task 15
|
||||||
|
|
||||||
|
We need to ensure that when pods are created in this cluster, they cannot use the latest image tag, irrespective of the repository being used.
|
||||||
|
|
||||||
|
To achieve this, a simple Admission Webhook Server has been developed and deployed. A service called image-bouncer-webhook is deployed in the cluster. This Webhook server ensures that the developers of the team cannot use the latest image tag. Use the following specs to integrate it with the cluster using an ImagePolicyWebhook:
|
||||||
|
|
||||||
|
Create a new admission configuration file at /etc/admission-controllers/admission-configuration.yaml
|
||||||
|
|
||||||
|
The kubeconfig file with the credentials to connect to the webhook server is located at /root/CKS/ImagePolicy/admission-kubeconfig.yaml. Note: The /root/CKS/ImagePolicy/ directory is already mounted on the kube-apiserver at path /etc/admission-controllers, so reference that path in reference the admission configuration.
|
||||||
|
|
||||||
|
Ensure that if the latest tag is used, the request must be rejected at all times.
|
||||||
|
|
||||||
|
Enable the Admission Controller.
|
||||||
|
|
||||||
|
Finally, delete the existing pod in the magnum namespace that violates the policy and recreate it, ensuring the same image but using tag 1.27.
|
||||||
|
|
||||||
|
NOTE: If the kube-apiserver becomes unresponsive, this can affect the validation of this exam. In such a case, restore the kube-apiserver using the backup file created at: /root/backup/kube-apiserver.yaml. Wait for the API to be available again and proceed.
|
||||||
|
|
||||||
|
Is the ImagePolicyWebhook enabled and the API server running?
|
||||||
|
Is the policy implemented?
|
||||||
|
Is the pod recreated with the correct image?
|
||||||
|
Is the pod running?
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Create the below admission-configuration.yaml inside /root/CKS/ImagePolicy directory in the controlplane node:
|
||||||
|
|
||||||
|
apiVersion: apiserver.config.k8s.io/v1
|
||||||
|
kind: AdmissionConfiguration
|
||||||
|
plugins:
|
||||||
|
- name: ImagePolicyWebhook
|
||||||
|
configuration:
|
||||||
|
imagePolicy:
|
||||||
|
kubeConfigFile: /etc/admission-controllers/admission-kubeconfig.yaml
|
||||||
|
allowTTL: 50
|
||||||
|
denyTTL: 50
|
||||||
|
retryBackoff: 500
|
||||||
|
defaultAllow: false
|
||||||
|
|
||||||
|
The /root/CKS/ImagePolicy is mounted at the path /etc/admission-controllers directory in the kube-apiserver. So, you can directly place the files under /root/CKS/ImagePolicy.
|
||||||
|
|
||||||
|
Here is a snippet of the volume and volumeMounts (already added to apiserver config, you don't need to do anything here):
|
||||||
|
|
||||||
|
containers:
|
||||||
|
.
|
||||||
|
.
|
||||||
|
.
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /etc/admission-controllers
|
||||||
|
name: admission-controllers
|
||||||
|
readOnly: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- hostPath:
|
||||||
|
path: /root/CKS/ImagePolicy/
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
name: admission-controllers
|
||||||
|
|
||||||
|
Next, update the kube-apiserver command flags and add ImagePolicyWebhook to the enable-admission-plugins flag. Use the configuration file that was created in the previous step as the value of admission-control-config-file.
|
||||||
|
|
||||||
|
Note: Remember, this command will be run inside the kube-apiserver container, so the path must be /etc/admission-controllers/admission-configuration.yaml (mounted from /root/CKS/ImagePolicy in controlplane).
|
||||||
|
|
||||||
|
Edit the kube-apiserver manifest file:
|
||||||
|
|
||||||
|
vi /etc/kubernetes/manifests/kube-apiserver.yaml
|
||||||
|
|
||||||
|
to add the following flags under spec.containers.command:
|
||||||
|
|
||||||
|
- --admission-control-config-file=/etc/admission-controllers/admission-configuration.yaml
|
||||||
|
- --enable-admission-plugins=NodeRestriction,ImagePolicyWebhook
|
||||||
|
|
||||||
|
In case we mess up while solving the question, API server could become unresponsive.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
The connection to the server controlplane:6443 was refused - did you specify the right host or port?
|
||||||
|
|
||||||
|
In such case scenario restore kube-apiserver to it's default state using the backup provided at /root/backup/kube-apiserver.yaml
|
||||||
|
Run the below command to restore the kube-apiserver initial state:
|
||||||
|
|
||||||
|
cp -v /root/backup/kube-apiserver.yaml /etc/kubernetes/manifests
|
||||||
|
|
||||||
|
Remediate the Non-Compliant Pod
|
||||||
|
After the ImagePolicyWebhook is active, you must fix the existing app-0403 pod in the magnum namespace, as it violates the new policy, and , Admission controllers only check new requests; they don't affect pods that are already running.
|
||||||
|
|
||||||
|
Get the Pod's Definition
|
||||||
|
First, get the YAML definition of the running pod and save it to a file.
|
||||||
|
kubectl get pod app-0403 -n magnum -o yaml > app-0403-fix.yaml
|
||||||
|
|
||||||
|
Clean and Edit the YAML
|
||||||
|
Now, edit the file. The YAML you just saved contains live, system-managed fields. You must clean it up before you can re-apply it.
|
||||||
|
vi app-0403-fix.yaml
|
||||||
|
|
||||||
|
Change:
|
||||||
|
|
||||||
|
image: gcr.io/google-containers/busybox
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
image: gcr.io/google-containers/busybox:1.27
|
||||||
|
|
||||||
|
Save and exit the file.
|
||||||
|
|
||||||
|
Delete the Old Pod
|
||||||
|
You must delete the running pod before you can create a new one with the same name.
|
||||||
|
kubectl delete pod app-0403 -n magnum
|
||||||
|
|
||||||
|
Apply the Fixed Manifest
|
||||||
|
Finally, create the new, compliant pod from your corrected YAML file. The webhook will inspect this request and allow it.
|
||||||
|
kubectl apply -f app-0403-fix.yaml
|
||||||
104
kubernetes/cks/kodekloud-mock-3.md
Normal file
104
kubernetes/cks/kodekloud-mock-3.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# Task 1
|
||||||
|
|
||||||
|
Weight: 3
|
||||||
|
You are setting up a new Kubernetes cluster and need to secure Docker as part of the cluster setup.
|
||||||
|
|
||||||
|
Ensure that docker runs under the "root" group and that no external TCP connections are allowed to the docker daemon.
|
||||||
|
|
||||||
|
Ensure the configuration is persistent across restarts.
|
||||||
|
|
||||||
|
|
||||||
|
Is docker modified to run as part of the root group?
|
||||||
|
Has the docker daemon been modified to have no external TCP connections?
|
||||||
|
|
||||||
|
# Task 3
|
||||||
|
|
||||||
|
Create a service account named bot-sa in the automated namespace. Ensure that it does not get automatically mounted to workloads.
|
||||||
|
|
||||||
|
A workload named sweeper also exists in the same namespace. Update this deployment to use the newly created service account, and mount the service account token as a projected volume. Do not change any other fields in the deployment.
|
||||||
|
|
||||||
|
Has the service account bot-sa been created without auto-mount?
|
||||||
|
Has the bot-sa service account been set as the deployment's service account?
|
||||||
|
Has the bot-sa service account token been mounted to the deployment as a projected volume?
|
||||||
|
No default auto-mount on the Pod ?
|
||||||
|
|
||||||
|
|
||||||
|
# Task 6
|
||||||
|
|
||||||
|
You have an existing Kubernetes setup with the following services running:
|
||||||
|
|
||||||
|
Namespace:
|
||||||
|
system-hardening
|
||||||
|
|
||||||
|
Pods:
|
||||||
|
nginx-internal (Accessible internally)
|
||||||
|
nginx-external (Exposed externally via NodePort
|
||||||
|
service)
|
||||||
|
|
||||||
|
Services:
|
||||||
|
nginx-internal-service (Exposed as ClusterIP - internal-only)
|
||||||
|
nginx-external-service (Exposed as NodePort - accessible externally)
|
||||||
|
|
||||||
|
Objective:
|
||||||
|
Your task is to disable or unexpose ports to minimize external access to unnecessary services.
|
||||||
|
|
||||||
|
Has the service type for nginx-external-service been changed to ClusterIP?
|
||||||
|
|
||||||
|
# Task 7
|
||||||
|
|
||||||
|
A deployment named web-server is running in namespace restricted.
|
||||||
|
|
||||||
|
Identify why the deployment is not in a running state, and then fix the issue so that it is in a running state.
|
||||||
|
|
||||||
|
Has the deployment been fixed, and is the pod in a running state?
|
||||||
|
|
||||||
|
# Task 8
|
||||||
|
|
||||||
|
Configure the kubelet on the cluster2-controlplane node to disallow anonymous authentication.
|
||||||
|
|
||||||
|
The admin kubeconfig file for this cluster is located at:
|
||||||
|
/root/custom-config/admin.conf
|
||||||
|
|
||||||
|
Additionally, utilize this kubeconfig file to delete the role custom-role in namespace delta.
|
||||||
|
|
||||||
|
Ensure that, from the node, the cluster cannot be accessed with kubectl unless the --kubeconfig=/root/custom-config/admin.conf flag is explicitly provided.
|
||||||
|
|
||||||
|
|
||||||
|
Has anonymous auth been disabled on the kubelet?
|
||||||
|
Is the cluster not accessible without passing the --kubeconfig flag with kubectl?
|
||||||
|
Has the custom-role been deleted?
|
||||||
|
|
||||||
|
# Task 10
|
||||||
|
|
||||||
|
In the space namespace, a deployment rocket-server is exposed by a service of the same name.
|
||||||
|
|
||||||
|
Create an ingress resource named rocket-ingress to load balance the incoming traffic to the workload on path /.
|
||||||
|
|
||||||
|
Use the hostname rocket-server.local for the Ingress rules.
|
||||||
|
|
||||||
|
Utilize the TLS certificate stored in the secret rocket-tls in the space namespace to enable TLS traffic on that ingress resource.
|
||||||
|
|
||||||
|
Is the ingress resource correctly created?
|
||||||
|
Has the ingress TLS secret been attached?
|
||||||
|
Does the ingress route to the correct path and port?
|
||||||
|
Is curl working for the HTTPS connection via the ingress?
|
||||||
|
|
||||||
|
# Task 14
|
||||||
|
|
||||||
|
Enable auditing on this cluster using the basic policy file available at /etc/kubernetes/cluster-policy.yaml.
|
||||||
|
|
||||||
|
Store the logs at /var/log/cluster-audit.log and ensure they are retained for 10 days. The log size should not exceed 10MB and up to a maximum of 3 files should be kept at a time.
|
||||||
|
|
||||||
|
After enabling auditing on the cluster, update the basic policy file to track the following:
|
||||||
|
|
||||||
|
Delete activity on secrets in the kube-system namespace at the Metadata level
|
||||||
|
Changes to deployments in the default namespace at the Request level
|
||||||
|
All other requests at the Metadata level
|
||||||
|
Ensure that the updated policy is applied and in effect.
|
||||||
|
|
||||||
|
Note: A copy of the kube-apiserver.yaml is available in ~/ so you can revert if the configuration goes wrong. Ensure that the kube-apiserver is working correctly, as it will be required for grading the exam.
|
||||||
|
|
||||||
|
Does /var/log/cluster-audit.log exist with the logs written to it?
|
||||||
|
Does auditing follow the mentioned parameters as specified in the question?
|
||||||
|
Is the delete activity on secrets tracked at the Metadata level in the kube-system namespace?
|
||||||
|
Is the edit activity on deployments tracked at the Request level in the default namespace?
|
||||||
242
kubernetes/cks/kodeklouid-mock-2.md
Normal file
242
kubernetes/cks/kodeklouid-mock-2.md
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
# Task 1
|
||||||
|
|
||||||
|
A pod called redis-backend has been created in the prod-x12cs namespace. It has been exposed as a service of type ClusterIP. The pod listens on TCP port 6379.
|
||||||
|
|
||||||
|
Create a network policy called allow-redis-access to lock down access to this pod only for the following:
|
||||||
|
|
||||||
|
Any pod in the same namespace with the label backend=prod-x12cs
|
||||||
|
|
||||||
|
All pods in the prod-yx13cs namespace
|
||||||
|
|
||||||
|
Ensure that traffic is only allowed on TCP port 6379.
|
||||||
|
|
||||||
|
All other incoming connections should be blocked.
|
||||||
|
|
||||||
|
Use the existing labels when creating the network policy.
|
||||||
|
|
||||||
|
Is Network Policy applied on the correct pods?
|
||||||
|
Is the incoming traffic allowed from pods in prod-yx13cs namespace?
|
||||||
|
Is the incoming traffic allowed from pods with label backend=prod-x12cs ?
|
||||||
|
Is the ingress traffic restricted to port 6379?
|
||||||
|
|
||||||
|
|
||||||
|
# Task 2
|
||||||
|
|
||||||
|
There is an existing CiliumNetworkPolicy
|
||||||
|
default-allow in the namespace team-azure, which allows all traffic.
|
||||||
|
|
||||||
|
In the namespace team-azure, create a CiliumNetworkPolicy as follows:
|
||||||
|
|
||||||
|
Create a Layer 3 policy named p1 that denies outgoing traffic from Pods with the label role=messenger to Pods with the label role=database.
|
||||||
|
|
||||||
|
Is the cilium policy created ?
|
||||||
|
Is the cilium network policy working as expected?
|
||||||
|
|
||||||
|
# Task 3
|
||||||
|
|
||||||
|
A pod named apps-cluster-dash has been created in the gamma namespace using a service account called cluster-view. This service account has been granted additional permissions as compared to the default service account and can view resources cluster-wide on this Kubernetes cluster. While these permissions are important for the application in this pod to work, the secret token is still mounted on this pod.
|
||||||
|
|
||||||
|
|
||||||
|
Secure the pod in such a way that the secret token is no longer mounted on this pod. You may delete and recreate the pod.
|
||||||
|
|
||||||
|
Is the pod created with a cluster-view service account?
|
||||||
|
Is the secret token not mounted in the pod?
|
||||||
|
|
||||||
|
# Task 4
|
||||||
|
|
||||||
|
A pod in the sahara namespace has generated alerts that a shell was opened inside the container.
|
||||||
|
|
||||||
|
|
||||||
|
To recognize such alerts, set the priority to ALERT and change the format of the output so that it looks like the below:
|
||||||
|
|
||||||
|
ALERT timestamp of the event without nanoseconds,User ID,the container id,the container image repository
|
||||||
|
|
||||||
|
Make sure to update the rule such that the changes persist across Falco updates.
|
||||||
|
|
||||||
|
You can refer the falco documentation Here
|
||||||
|
|
||||||
|
Are the rules updated according to the new format?
|
||||||
|
Is falco running?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Task 5
|
||||||
|
|
||||||
|
martin is a developer who needs access to work on the dev-a, dev-b, and dev-z namespaces. He should be able to perform any operation on any pod in the dev-a and dev-b namespaces. However, in the dev-z namespace, he should only have permission to get and list the pods.
|
||||||
|
|
||||||
|
The current setup is too permissive and violates the above condition. Use the above requirement to secure martin's access in the cluster. You may re-create objects, but ensure they retain their existing names.
|
||||||
|
|
||||||
|
Does martin have unrestricted access to all pods in dev-a?
|
||||||
|
Does martin have unrestricted access to all pods in dev-b?
|
||||||
|
Can martin only list and get pods in dev-z?
|
||||||
|
|
||||||
|
# Task 6
|
||||||
|
|
||||||
|
There is a deployment named hacker in the namespace team-red, which mounts /run/containerd as a hostPath volume on the Node where it's running.
|
||||||
|
This means that the Pod can access various data about other containers running on the same Node.
|
||||||
|
|
||||||
|
To prevent this, configure the team-red namespace to enforce the baseline Pod Security Standard. Once completed, delete the Pod from the
|
||||||
|
deployment mentioned above.
|
||||||
|
|
||||||
|
Check the ReplicaSet events and write the event lines containing the reason why the Pod isn't recreated into /opt/course/logs.txt.
|
||||||
|
|
||||||
|
Note: You may see multiple identical event lines with the same error. Paste only one of them in the logs.txt file - preferably the last one.
|
||||||
|
|
||||||
|
Is the pod in the team-red namespace deleted?
|
||||||
|
Are the replicaset events written to /opt/course/logs.txt?
|
||||||
|
|
||||||
|
# Task 7
|
||||||
|
|
||||||
|
There is a Dockerfile named unsecure.Dockerfile located at /opt/course/image/.
|
||||||
|
|
||||||
|
DevSecOps has asked you to improve this image by:
|
||||||
|
|
||||||
|
Changing the base image to alpine:3.12
|
||||||
|
Not installing curl
|
||||||
|
Updating nginx to use the version constraint >=1.18.0
|
||||||
|
Running the main process as user myuser
|
||||||
|
|
||||||
|
Do not add any new lines to the Dockerfile - only modify the existing ones.
|
||||||
|
|
||||||
|
Is the Dockerfile modified?
|
||||||
|
|
||||||
|
# Task 8
|
||||||
|
|
||||||
|
A pod definition file has been created at /root/CKS/simple-pod.yaml . Use the kubesec tool to generate a report for this pod definition file and fix the major issues so that the subsequent scan passes.
|
||||||
|
|
||||||
|
|
||||||
|
Once done, generate the report again and save it to the file /root/CKS/kubesec-report.txt
|
||||||
|
|
||||||
|
Is the pod definition file fixed?
|
||||||
|
Is the pass report generated and saved at /root/CKS/kubesec-report.txt?
|
||||||
|
|
||||||
|
# Task 9
|
||||||
|
|
||||||
|
There is a deployment named rocket-server, which is exposed by a service of the same name in the space namespace.
|
||||||
|
|
||||||
|
Create an ingress resource named rocket-ingress to load balance the incoming traffic to the workload on path /.
|
||||||
|
|
||||||
|
Use the hostname rocket-server.local for the Ingress rules.
|
||||||
|
|
||||||
|
Use the TLS certificate stored in the rocket-tls secret in the space namespace to enable TLS traffic on the ingress resource.
|
||||||
|
|
||||||
|
|
||||||
|
Make sure to add the IP of the Ingress Nginx controller matching the rocket-server.local DNS to the /etc/hosts file.
|
||||||
|
|
||||||
|
Note: This cluster has no external load balancer provider, so the EXTERNAL-IP of the ingress-nginx-controller service will stay in the <pending> state - this is expected. Use the IP under the CLUSTER-IP column of that service for the /etc/hosts entry.
|
||||||
|
|
||||||
|
Is the ingress resource correctly created?
|
||||||
|
Has the ingress TLS secret been attached?
|
||||||
|
Does the ingress route to the correct path and port?
|
||||||
|
Is curl working for the HTTPS connection via the ingress?
|
||||||
|
|
||||||
|
# Task 10
|
||||||
|
|
||||||
|
Create a new pod called audit-nginx in the default namespace using the nginx image. Secure the syscalls that this pod can use by using the audit.json seccomp profile in the pod's security context.
|
||||||
|
|
||||||
|
|
||||||
|
The audit.json file is located in the /root/CKS directory.
|
||||||
|
|
||||||
|
Before creating the pod, move the file to the profiles directory inside the default seccomp directory.
|
||||||
|
|
||||||
|
Does audit-nginx use the right image?
|
||||||
|
Is the pod running?
|
||||||
|
Does the pod use the correct seccomp profile?
|
||||||
|
|
||||||
|
# Task 11
|
||||||
|
|
||||||
|
A deployment named fruits in the namespace salad has three containers:
|
||||||
|
|
||||||
|
apple
|
||||||
|
banana, and
|
||||||
|
kiwi
|
||||||
|
One of these containers has the package curl installed. Identify which container has that package from the running containers, and create an SBOM SPDX for the container's image.
|
||||||
|
|
||||||
|
Use the tarball archive of that particular image stored under /root/ImageTarballs directory for generating the SPDX JSON. The archives have names matching their images.
|
||||||
|
|
||||||
|
Save the output in ~/bugged-fruit.spdx. Save the container name in ~/bugged-container.txt.
|
||||||
|
|
||||||
|
Note: bom and all its required dependencies are already installed.
|
||||||
|
|
||||||
|
Is SPDX JSON SBOM for the image with curl installed, stored at ~/bugged-fruit.spdx?
|
||||||
|
Is the container name stored in ~/bugged-container.txt?
|
||||||
|
|
||||||
|
# Task 12
|
||||||
|
|
||||||
|
Enable auditing on this cluster using a basic policy file located at /etc/kubernetes/cluster-policy.yaml.
|
||||||
|
|
||||||
|
Configure the logs to be stored at /var/log/cluster-audit.log and retained for 10 days. The maximum size should be 10MB, and up to 3 files should be kept at a time.
|
||||||
|
|
||||||
|
After enabling auditing on the cluster, update the basic policy file to track the following:
|
||||||
|
|
||||||
|
Delete activity on secrets in the kube-system namespace at the Metadata level
|
||||||
|
Changes to deployments in the default namespace at the Request level
|
||||||
|
All other requests at the Metadata level
|
||||||
|
Ensure that the updated policy file is applied and in effect.
|
||||||
|
|
||||||
|
Note: A copy of the kube-apiserver.yaml is available in ~/ so you can revert if the configuration goes wrong. Ensure that the kube-apiserver is working correctly, as it will be required for grading the exam.
|
||||||
|
|
||||||
|
|
||||||
|
Does /var/log/cluster-audit.log exist with the logs written to it?
|
||||||
|
Does auditing follow the mentioned parameters as specified in the question?
|
||||||
|
Is the delete activity on secrets tracked at the Metadata level in the kube-system namespace?
|
||||||
|
Is the edit activity on deployments tracked at the Request level in the default namespace?
|
||||||
|
|
||||||
|
|
||||||
|
# Task 13
|
||||||
|
|
||||||
|
A pod has been created in the omni namespace, but it has a couple of issues.
|
||||||
|
|
||||||
|
The pod has been created with more permissions than it needs.
|
||||||
|
|
||||||
|
It allows read access in the /usr/share/nginx/html/internal directory, opening the internal site to be accessed publicly.
|
||||||
|
|
||||||
|
To verify this, click the Site button (above the terminal) and add /internal/ to the end of the URL.
|
||||||
|
|
||||||
|
Use the below recommendations to fix this:
|
||||||
|
|
||||||
|
|
||||||
|
Use the AppArmor profile created at /etc/apparmor.d/frontend to restrict the internal site.
|
||||||
|
|
||||||
|
Several service accounts exist in the omni namespace. Apply the principle of least privilege to select the service account with the minimum privileges (excluding the default service account).
|
||||||
|
|
||||||
|
Recreate the pod with the correct service account, and then delete the other unused service accounts in the omni namespace (excluding the default service account).
|
||||||
|
|
||||||
|
You can recreate the pod, but do not create a new service account and do not use the default service account.
|
||||||
|
|
||||||
|
Is the correct service account used?
|
||||||
|
Are the obsolete service accounts deleted?
|
||||||
|
Is the internal-site restricted?
|
||||||
|
Is the pod running?
|
||||||
|
|
||||||
|
|
||||||
|
# Task 14
|
||||||
|
|
||||||
|
The namespace encrypted has two applications, alpha and beta.
|
||||||
|
|
||||||
|
Since these applications handle critical communications, enforce strict mTLS using Istio in the encrypted namespace.
|
||||||
|
|
||||||
|
Make sure that the workloads have the istio sidecar injected.
|
||||||
|
|
||||||
|
|
||||||
|
Note: istio and istioctl have already been installed for you.
|
||||||
|
Has the istio-proxy sidecar been injected for the alpha deployment?
|
||||||
|
Has the istio-proxy sidecar been injected for the beta deployment?
|
||||||
|
Has the STRICT mTLS policy been applied?
|
||||||
|
|
||||||
|
# Task 15
|
||||||
|
|
||||||
|
In the namespace code, create a TLS secret code-secret with the following certificate and key provided:
|
||||||
|
|
||||||
|
cert: /root/custom-cert.crt
|
||||||
|
key: /root/custom-key.key
|
||||||
|
Attach that secret as a volume named secret-volume in the deployment code-server.
|
||||||
|
|
||||||
|
|
||||||
|
Is the code-secret secret created in the namespace code with the mentioned specs?
|
||||||
|
Is the secret mounted as a volume on the container in the deployment code-server?
|
||||||
|
|
||||||
|
## solution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl create secret tls code-secret -n code --cert=/root/custom-cert.crt --key=/root/custom-key.key
|
||||||
95
kubernetes/level 1/task-1.md
Normal file
95
kubernetes/level 1/task-1.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
The Nautilus DevOps team is diving into Kubernetes for application management. One team member has a task to create a pod according to the details below:
|
||||||
|
|
||||||
|
|
||||||
|
Create a pod named pod-httpd using the httpd image with the latest tag. Ensure to specify the tag as httpd:latest.
|
||||||
|
|
||||||
|
Set the app label to httpd_app, and name the container as httpd-container.
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Kubernetes Pod — `pod-httpd`
|
||||||
|
|
||||||
|
A single pod running the `httpd:latest` image, with an `app: httpd_app` label and a
|
||||||
|
named container. Applied inline via a heredoc — no manifest file on disk.
|
||||||
|
|
||||||
|
## Apply (heredoc → kubectl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: pod-httpd
|
||||||
|
labels:
|
||||||
|
app: httpd_app
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: httpd-container
|
||||||
|
image: httpd:latest
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The heredoc apply pattern
|
||||||
|
|
||||||
|
- **`kubectl apply -f -`** — the `-f -` tells kubectl to read the manifest from
|
||||||
|
**stdin** instead of a file. The heredoc feeds the YAML straight in, so nothing is
|
||||||
|
written to disk.
|
||||||
|
- **`<<'EOF'` (delimiter quoted)** — quoting `EOF` disables shell expansion inside the
|
||||||
|
body, so any `$VAR`, backticks, or `$(...)` in a manifest stay literal rather than
|
||||||
|
being interpreted by the shell. For Kubernetes YAML this is almost always what you
|
||||||
|
want. Use unquoted `<<EOF` only when you deliberately want the shell to interpolate a
|
||||||
|
variable into the manifest before it reaches kubectl.
|
||||||
|
- **Indentation still matters** — YAML inside the heredoc must keep exact indentation,
|
||||||
|
spaces not tabs.
|
||||||
|
|
||||||
|
### The manifest, field by field
|
||||||
|
|
||||||
|
- **`apiVersion: v1` / `kind: Pod`** — a Pod is a core (`v1`) object, the smallest
|
||||||
|
deployable unit in Kubernetes: one or more containers sharing a network namespace and
|
||||||
|
storage.
|
||||||
|
- **`metadata.name: pod-httpd`** — the pod's name, exactly as required.
|
||||||
|
- **`metadata.labels.app: httpd_app`** — the required label. Labels are key/value tags
|
||||||
|
used by selectors (Services, ReplicaSets, `kubectl get -l`) to target the pod, so
|
||||||
|
`kubectl get pods -l app=httpd_app` will match this pod.
|
||||||
|
- **`spec.containers`** — the list of containers in the pod, here a single entry:
|
||||||
|
- **`name: httpd-container`** — the container name, exactly as required. This is the
|
||||||
|
name you'd pass to `kubectl logs` / `kubectl exec -c`.
|
||||||
|
- **`image: httpd:latest`** — the Apache HTTP Server image at the `latest` tag,
|
||||||
|
specified explicitly as required. Writing just `httpd` would default the tag to
|
||||||
|
`latest` anyway, but the task asks for the tag to be stated, so it's spelled out.
|
||||||
|
|
||||||
|
### Label vs. name — two different things
|
||||||
|
|
||||||
|
Easy to conflate, but distinct:
|
||||||
|
- the **pod name** (`pod-httpd`) uniquely identifies the pod in its namespace, and
|
||||||
|
- the **`app` label** (`httpd_app`) is a selector tag that can be shared across many
|
||||||
|
pods.
|
||||||
|
|
||||||
|
They live in different manifest fields (`metadata.name` vs `metadata.labels`) and serve
|
||||||
|
different purposes.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pod is running with the correct name
|
||||||
|
kubectl get pod pod-httpd -o wide
|
||||||
|
|
||||||
|
# Label is set
|
||||||
|
kubectl get pod pod-httpd --show-labels
|
||||||
|
|
||||||
|
# Container name and image are correct
|
||||||
|
kubectl get pod pod-httpd \
|
||||||
|
-o jsonpath='{.spec.containers[0].name}{" "}{.spec.containers[0].image}{"\n"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — `pod-httpd` in `Running` status, labels showing `app=httpd_app`, and the
|
||||||
|
jsonpath printing `httpd-container httpd:latest`.
|
||||||
|
|
||||||
|
> If the pod sits in `ImagePullBackOff`, the node may be offline or rate-limited on the
|
||||||
|
> pull. `kubectl describe pod pod-httpd` shows the pull events under `Events:`.
|
||||||
144
kubernetes/level 1/task-10.md
Normal file
144
kubernetes/level 1/task-10.md
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
The Nautilus DevOps team needs a time check pod created in a specific Kubernetes namespace for logging purposes. Initially, it's for testing, but it may be integrated into an existing cluster later. Here's what's required:
|
||||||
|
|
||||||
|
|
||||||
|
Create a pod called time-check in the nautilus namespace. The pod should contain a container named time-check, utilizing the busybox image with the latest tag (specify as busybox:latest).
|
||||||
|
|
||||||
|
Create a config map named time-config with the data TIME_FREQ=7 in the same namespace.
|
||||||
|
|
||||||
|
Configure the time-check container to execute the command: while true; do date; sleep $TIME_FREQ;done. Ensure the result is written /opt/finance/time/time-check.log. Also, add an environmental variable TIME_FREQ in the container, fetching its value from the config map TIME_FREQ key.
|
||||||
|
|
||||||
|
Create a volume log-volume and mount it at /opt/finance/time within the container.
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Kubernetes Pod + ConfigMap — `time-check` in `nautilus`
|
||||||
|
|
||||||
|
A `time-check` pod that loops `date` into a log file on a mounted volume, reading its
|
||||||
|
sleep interval from a ConfigMap-backed env var. Applied inline via a multi-document
|
||||||
|
heredoc — no manifest file on disk.
|
||||||
|
|
||||||
|
## Apply (heredoc → kubectl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: nautilus
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: time-config
|
||||||
|
namespace: nautilus
|
||||||
|
data:
|
||||||
|
TIME_FREQ: "7"
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: time-check
|
||||||
|
namespace: nautilus
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: time-check
|
||||||
|
image: busybox:latest
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- "while true; do date; sleep $TIME_FREQ; done >> /opt/finance/time/time-check.log"
|
||||||
|
env:
|
||||||
|
- name: TIME_FREQ
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: time-config
|
||||||
|
key: TIME_FREQ
|
||||||
|
volumeMounts:
|
||||||
|
- name: log-volume
|
||||||
|
mountPath: /opt/finance/time
|
||||||
|
volumes:
|
||||||
|
- name: log-volume
|
||||||
|
emptyDir: {}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The heredoc apply pattern
|
||||||
|
|
||||||
|
- **`kubectl apply -f -`** reads from **stdin**; `---` separates the three documents,
|
||||||
|
processed in order so the **Namespace → ConfigMap → Pod** dependency chain is satisfied.
|
||||||
|
- **`<<'EOF'` (delimiter quoted)** is critical here: it stops the jump-host's shell from
|
||||||
|
expanding `$TIME_FREQ` when you paste the command. The literal `$TIME_FREQ` must reach the
|
||||||
|
manifest untouched so the **container's** shell expands it at runtime from the env var.
|
||||||
|
An unquoted heredoc would blank it out before it ever got to Kubernetes.
|
||||||
|
|
||||||
|
### The Namespace
|
||||||
|
|
||||||
|
- **`kind: Namespace` / `name: nautilus`** — created first so the ConfigMap and Pod have
|
||||||
|
somewhere to live. If `nautilus` already exists, `apply` is idempotent and simply no-ops.
|
||||||
|
|
||||||
|
### The ConfigMap
|
||||||
|
|
||||||
|
- **`name: time-config`, `namespace: nautilus`** — as required.
|
||||||
|
- **`data.TIME_FREQ: "7"`** — the key/value the pod reads. The value is **quoted** because
|
||||||
|
ConfigMap values must be strings; an unquoted `7` would be a YAML integer and rejected.
|
||||||
|
|
||||||
|
### The Pod
|
||||||
|
|
||||||
|
- **`name: time-check`, `namespace: nautilus`**, container **`name: time-check`**,
|
||||||
|
**`image: busybox:latest`** — all exactly as required.
|
||||||
|
|
||||||
|
- **`command`** — run via `/bin/sh -c` so the shell interprets the loop and, crucially,
|
||||||
|
expands `$TIME_FREQ`. The loop prints `date` every `$TIME_FREQ` seconds; the
|
||||||
|
`>> /opt/finance/time/time-check.log` redirect on the **whole loop** opens the log file
|
||||||
|
once and appends each timestamp to it continuously. That satisfies "the result is written
|
||||||
|
to /opt/finance/time/time-check.log."
|
||||||
|
|
||||||
|
- **`env` with `valueFrom.configMapKeyRef`** — injects an env var named `TIME_FREQ` whose
|
||||||
|
value is pulled from the `TIME_FREQ` key of the `time-config` ConfigMap. This is the link
|
||||||
|
between requirement 2 and the `$TIME_FREQ` in the command — the container gets `7` at
|
||||||
|
runtime without hardcoding it.
|
||||||
|
|
||||||
|
- **`volumeMounts` + `volumes`** — the two halves of attaching storage:
|
||||||
|
- **`volumes: - name: log-volume / emptyDir: {}`** declares a volume named `log-volume`.
|
||||||
|
`emptyDir` is an ephemeral, pod-lifetime scratch volume — the simplest choice for a log
|
||||||
|
location the task doesn't require to persist.
|
||||||
|
- **`volumeMounts: - name: log-volume / mountPath: /opt/finance/time`** mounts that volume
|
||||||
|
into the container at `/opt/finance/time`, which is where the log file is written. The
|
||||||
|
`name` on both sides must match, or Kubernetes can't wire them together.
|
||||||
|
|
||||||
|
### Why the mount path and log path line up
|
||||||
|
|
||||||
|
The command writes to `/opt/finance/time/time-check.log`, and the volume is mounted at
|
||||||
|
`/opt/finance/time`. So the log file lands **inside** the mounted volume — meaning the
|
||||||
|
directory exists (the mount creates it) and the write succeeds. If the mount path and the
|
||||||
|
log directory didn't match, the write could fail or land on the container's ephemeral
|
||||||
|
root filesystem instead.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ConfigMap present with the value
|
||||||
|
kubectl get configmap time-config -n nautilus -o jsonpath='{.data.TIME_FREQ}{"\n"}'
|
||||||
|
|
||||||
|
# Pod running
|
||||||
|
kubectl get pod time-check -n nautilus -o wide
|
||||||
|
|
||||||
|
# Env var resolved from the ConfigMap
|
||||||
|
kubectl exec time-check -n nautilus -- printenv TIME_FREQ
|
||||||
|
|
||||||
|
# Log file is being written inside the mounted volume
|
||||||
|
kubectl exec time-check -n nautilus -- cat /opt/finance/time/time-check.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — ConfigMap value `7`, pod `Running`, `printenv` showing `TIME_FREQ=7`, and the
|
||||||
|
log file accumulating timestamps roughly every 7 seconds.
|
||||||
|
|
||||||
|
> Remember `-n nautilus` on every command — nothing here is in `default`. If the pod is
|
||||||
|
> `Error`/`CrashLoopBackOff`, check `kubectl logs time-check -n nautilus` (though output goes
|
||||||
|
> to the log file, so the pod's stdout may be quiet).
|
||||||
133
kubernetes/level 1/task-11.md
Normal file
133
kubernetes/level 1/task-11.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
A junior DevOps team member encountered difficulties deploying a stack on the Kubernetes cluster. The pod fails to start, presenting errors. Let's troubleshoot and rectify the issue promptly.
|
||||||
|
|
||||||
|
|
||||||
|
There is a pod named webserver, and the container within it is named httpd-container, its utilizing the httpd:latest image.
|
||||||
|
|
||||||
|
Additionally, there's a sidecar container named sidecar-container using the ubuntu:latest image.
|
||||||
|
|
||||||
|
Identify and address the issue to ensure the pod is in the running state and the application is accessible.
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
There is incorrect image in the pod ... just fix it
|
||||||
|
|
||||||
|
# Troubleshoot Pod — `webserver` (httpd + ubuntu sidecar)
|
||||||
|
|
||||||
|
The `webserver` pod won't start. This walks the diagnosis, then applies a corrected
|
||||||
|
manifest via heredoc.
|
||||||
|
|
||||||
|
## Step 1 — Diagnose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Overall state + which container is failing
|
||||||
|
kubectl get pod webserver -o wide
|
||||||
|
|
||||||
|
# The authoritative source: events + per-container state at the bottom
|
||||||
|
kubectl describe pod webserver
|
||||||
|
|
||||||
|
# Logs from each container (the sidecar is the usual culprit)
|
||||||
|
kubectl logs webserver -c httpd-container
|
||||||
|
kubectl logs webserver -c sidecar-container
|
||||||
|
```
|
||||||
|
|
||||||
|
Look at the container states in `describe`: `ImagePullBackOff` / `ErrImagePull` points to
|
||||||
|
a **bad image name or tag**; `CrashLoopBackOff` with the sidecar exiting `Completed` points
|
||||||
|
to a **container with no long-running process**.
|
||||||
|
|
||||||
|
## Step 2 — The two usual root causes
|
||||||
|
|
||||||
|
For this specific setup (an `httpd` container plus an `ubuntu` sidecar), the failure is
|
||||||
|
almost always one or both of:
|
||||||
|
|
||||||
|
1. **Image typo** — e.g. `httpd:latst`, `httpd:letest`, or a misspelled `ubuntu` — which
|
||||||
|
yields `ErrImagePull` / `ImagePullBackOff`.
|
||||||
|
2. **The ubuntu sidecar exits immediately.** `ubuntu:latest` has no long-running entrypoint
|
||||||
|
— it starts a shell, finds nothing to do, and exits `0`. Kubernetes sees the container
|
||||||
|
terminate and puts the pod in `CrashLoopBackOff` (it keeps restarting a container that
|
||||||
|
keeps exiting). A sidecar **must** be given a command that keeps it alive.
|
||||||
|
|
||||||
|
## Step 3 — Fix (recreate with a corrected manifest)
|
||||||
|
|
||||||
|
A pod's container image and command are effectively immutable in place, so the clean fix is
|
||||||
|
delete and re-create:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl delete pod webserver
|
||||||
|
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: webserver
|
||||||
|
labels:
|
||||||
|
app: httpd
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: httpd-container
|
||||||
|
image: httpd:latest
|
||||||
|
- name: sidecar-container
|
||||||
|
image: ubuntu:latest
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- "while true; do echo sidecar running; sleep 5; done"
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The heredoc apply pattern
|
||||||
|
|
||||||
|
- **`kubectl apply -f -`** reads the manifest from **stdin**; nothing written to disk.
|
||||||
|
- **`<<'EOF'` (delimiter quoted)** keeps the `while`/`$`-free command literal so the
|
||||||
|
container's shell runs it as written.
|
||||||
|
|
||||||
|
### What the corrected manifest fixes
|
||||||
|
|
||||||
|
- **`httpd-container` → `image: httpd:latest`** — a valid, correctly-spelled image/tag, so
|
||||||
|
the pull succeeds (fixes any `ImagePullBackOff` from a typo).
|
||||||
|
- **`sidecar-container` → `command: [...while true...sleep 5...]`** — gives the ubuntu
|
||||||
|
container a **long-running foreground process**. Now it never exits, so the pod stays
|
||||||
|
`Running` instead of crash-looping. Any equivalent keep-alive works (`sleep infinity`,
|
||||||
|
`tail -f /dev/null`); the infinite loop is a clear, portable choice.
|
||||||
|
|
||||||
|
### Why a sidecar needs a command but httpd doesn't
|
||||||
|
|
||||||
|
`httpd:latest`'s default entrypoint **is** a long-running server (Apache in the
|
||||||
|
foreground), so it stays up on its own. `ubuntu:latest` has no such default — its job here
|
||||||
|
is just to accompany the main container, so **you** must supply the process that keeps it
|
||||||
|
alive. This asymmetry is the heart of the bug: the same manifest that's fine for httpd
|
||||||
|
leaves ubuntu dead on arrival.
|
||||||
|
|
||||||
|
### "Application accessible"
|
||||||
|
|
||||||
|
Once both containers stay up, the pod reports `2/2 Running` and Apache serves on its
|
||||||
|
default port 80 inside the pod. You can confirm the app responds from within the pod
|
||||||
|
(below); exposing it externally would be a separate Service, which this task doesn't ask
|
||||||
|
for.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Both containers up: READY should show 2/2
|
||||||
|
kubectl get pod webserver
|
||||||
|
|
||||||
|
# No more restart churn / crash events
|
||||||
|
kubectl describe pod webserver | sed -n '/Events/,$p'
|
||||||
|
|
||||||
|
# httpd actually serving inside the pod
|
||||||
|
kubectl exec webserver -c httpd-container -- sh -c 'apt-get -v >/dev/null 2>&1; echo ok' 2>/dev/null || true
|
||||||
|
kubectl exec webserver -c sidecar-container -- echo "sidecar alive"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — `webserver` in `Running` with `READY 2/2`, no crash-loop events, and both
|
||||||
|
`exec` checks succeeding. Apache serving on port 80 within the pod.
|
||||||
|
|
||||||
|
> If `describe` still shows `ImagePullBackOff` after the fix, the node is offline or
|
||||||
|
> rate-limited on the registry — not a manifest problem. If the sidecar still exits,
|
||||||
|
> confirm its `command` made it into the spec:
|
||||||
|
> `kubectl get pod webserver -o jsonpath='{.spec.containers[1].command}'`.
|
||||||
103
kubernetes/level 1/task-12.md
Normal file
103
kubernetes/level 1/task-12.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
An application deployed on the Kubernetes cluster requires an update with new features developed by the Nautilus application development team. The existing setup includes a deployment named nginx-deployment and a service named nginx-service. Below are the necessary changes to be implemented without deleting the deployment and service:
|
||||||
|
|
||||||
|
|
||||||
|
1.) Modify the service nodeport from 30008 to 32165
|
||||||
|
|
||||||
|
2.) Change the replicas count from 1 to 5
|
||||||
|
|
||||||
|
3.) Update the image from nginx:1.19 to nginx:latest
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Update Deployment + Service (no delete, all patches) — `nginx-deployment` / `nginx-service`
|
||||||
|
|
||||||
|
Three in-place changes, each as a surgical `kubectl patch`: replicas `1 → 5`, image
|
||||||
|
`nginx:1.19 → nginx:latest`, service nodePort `30008 → 32165`. No object is deleted and no
|
||||||
|
full manifest is reproduced.
|
||||||
|
|
||||||
|
## Patches
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) Replicas 1 -> 5 (strategic-merge patch)
|
||||||
|
kubectl patch deployment nginx-deployment \
|
||||||
|
-p '{"spec":{"replicas":5}}'
|
||||||
|
|
||||||
|
# 2) Image nginx:1.19 -> nginx:latest (strategic-merge, container matched by name)
|
||||||
|
kubectl patch deployment nginx-deployment \
|
||||||
|
-p '{"spec":{"template":{"spec":{"containers":[{"name":"nginx-container","image":"nginx:latest"}]}}}}'
|
||||||
|
|
||||||
|
# 3) NodePort 30008 -> 32165 (JSON patch, precise field replace)
|
||||||
|
kubectl patch service nginx-service \
|
||||||
|
--type=json \
|
||||||
|
-p='[{"op":"replace","path":"/spec/ports/0/nodePort","value":32165}]'
|
||||||
|
|
||||||
|
# Wait for the image change to roll out
|
||||||
|
kubectl rollout status deployment/nginx-deployment
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### Two patch types, chosen per change
|
||||||
|
|
||||||
|
`kubectl patch` supports different strategies; each change uses the one that's cleanest for it.
|
||||||
|
|
||||||
|
**Strategic-merge patch (default)** — used for replicas and image. It deep-merges the JSON
|
||||||
|
fragment into the live object, and it understands Kubernetes list semantics.
|
||||||
|
|
||||||
|
- **Replicas** — `{"spec":{"replicas":5}}` merges a single scalar field; nothing else in the
|
||||||
|
spec is touched.
|
||||||
|
- **Image** — `{"spec":{"template":{"spec":{"containers":[{"name":"nginx-container","image":"nginx:latest"}]}}}}`.
|
||||||
|
The `containers` list is merged **by the `name` key**, so including `name: nginx-container`
|
||||||
|
tells Kubernetes to patch *that* existing container's image rather than replace the whole
|
||||||
|
list or add a second container. This is why the correct container name matters — a wrong
|
||||||
|
name would append a new container instead of updating the existing one.
|
||||||
|
|
||||||
|
**JSON patch (`--type=json`)** — used for the nodePort. It's an ordered list of explicit
|
||||||
|
operations (RFC 6902). `replace` on `/spec/ports/0/nodePort` targets exactly one field of the
|
||||||
|
first port entry. JSON patch is the right tool for editing **one element of a list** like
|
||||||
|
`ports` — a strategic-merge patch on a ports array is ambiguous about how to match entries,
|
||||||
|
whereas `ports/0` is unambiguous. `32165` is inside the valid NodePort range
|
||||||
|
(`30000–32767`), so the API accepts it.
|
||||||
|
|
||||||
|
### Why the image patch triggers a rolling update
|
||||||
|
|
||||||
|
Changing the container image mutates the pod template. The Deployment controller detects the
|
||||||
|
template change, creates a new ReplicaSet, and rolls `nginx:latest` pods in while retiring the
|
||||||
|
`nginx:1.19` pods incrementally — governed by the live `RollingUpdate` strategy
|
||||||
|
(`maxSurge/maxUnavailable 25%`). `rollout status` blocks until that completes. The replicas
|
||||||
|
patch simply scales the ReplicaSet to 5.
|
||||||
|
|
||||||
|
### Nothing gets deleted
|
||||||
|
|
||||||
|
All three are `patch` operations that mutate existing objects in place. The service keeps its
|
||||||
|
ClusterIP; the deployment keeps its identity and rollout history. "Without deleting" is
|
||||||
|
satisfied by construction — a delete + recreate would drop the ClusterIP and history.
|
||||||
|
|
||||||
|
### Equivalent shortcuts
|
||||||
|
|
||||||
|
The same results are achievable with purpose-built verbs, if you prefer them over raw patches:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl scale deployment nginx-deployment --replicas=5
|
||||||
|
kubectl set image deployment/nginx-deployment nginx-container=nginx:latest
|
||||||
|
# (nodePort still needs a patch/edit — there's no dedicated verb for it)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get deployment nginx-deployment
|
||||||
|
kubectl get deployment nginx-deployment \
|
||||||
|
-o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
|
||||||
|
kubectl get service nginx-service \
|
||||||
|
-o jsonpath='{.spec.ports[0].nodePort}{"\n"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — deployment `READY 5/5`, image `nginx:latest`, nodePort `32165`.
|
||||||
|
|
||||||
|
> If the rollout hangs with new pods in `ImagePullBackOff`, the `nginx:latest` pull failed
|
||||||
|
> (node offline / rate-limited); `kubectl describe pod <name>` shows the cause.
|
||||||
106
kubernetes/level 1/task-13.md
Normal file
106
kubernetes/level 1/task-13.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
The Nautilus DevOps team has already deployed a ReplicaSet to host an application that requires a highly available infrastructure. Your task is to expose the application running in the existing ReplicaSet by creating a Kubernetes NodePort Service.
|
||||||
|
|
||||||
|
Follow the specifications below to create the Service and ensure the application pods are accessible:
|
||||||
|
|
||||||
|
|
||||||
|
A ReplicaSet named httpd-replicaset is already running in the cluster.
|
||||||
|
|
||||||
|
The pods managed by the ReplicaSet use the following labels:
|
||||||
|
Assign labels app as httpd_app, and type as front-end.
|
||||||
|
|
||||||
|
Create a NodePort Service named httpd-service to expose the application.
|
||||||
|
|
||||||
|
Set the NodePort to 30080.
|
||||||
|
|
||||||
|
Expose port 80 of the application.
|
||||||
|
|
||||||
|
Note: Do not delete or modify the configuration of the deployed ReplicaSet application.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Kubernetes NodePort Service — `httpd-service`
|
||||||
|
|
||||||
|
A NodePort Service that exposes the pods already managed by the `httpd-replicaset`, matched
|
||||||
|
by their labels. Applied inline via a heredoc — no manifest file on disk, and the ReplicaSet
|
||||||
|
is left untouched.
|
||||||
|
|
||||||
|
## Apply (heredoc → kubectl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: httpd-service
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
selector:
|
||||||
|
app: httpd_app
|
||||||
|
type: front-end
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 80
|
||||||
|
nodePort: 30080
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The heredoc apply pattern
|
||||||
|
|
||||||
|
- **`kubectl apply -f -`** reads from **stdin**; nothing written to disk.
|
||||||
|
- **`<<'EOF'` (delimiter quoted)** disables shell expansion — the right default for k8s YAML.
|
||||||
|
|
||||||
|
### A Service finds pods by label selector
|
||||||
|
|
||||||
|
A Service has no direct link to the ReplicaSet — it targets **pods** whose labels match its
|
||||||
|
`selector`. This is the crux of the task:
|
||||||
|
|
||||||
|
- **`selector: {app: httpd_app, type: front-end}`** — must exactly match the labels on the
|
||||||
|
pods the ReplicaSet manages. Any pod carrying **both** labels becomes an endpoint of this
|
||||||
|
Service, regardless of what created it. Since the ReplicaSet stamps its pods with these
|
||||||
|
labels, the Service automatically picks them up. A mismatched selector would yield a Service
|
||||||
|
with **zero endpoints** — it'd exist but route nowhere.
|
||||||
|
|
||||||
|
This is also why we don't touch the ReplicaSet: the Service attaches by label, so exposing the
|
||||||
|
app needs no change to the existing workload at all.
|
||||||
|
|
||||||
|
### NodePort and the port fields
|
||||||
|
|
||||||
|
`type: NodePort` opens a port on **every node** that forwards to the Service, which in turn
|
||||||
|
load-balances across the matching pods. The three port fields each mean something distinct:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
|-------|---------|
|
||||||
|
| `port: 80` | The port the **Service** itself listens on (its ClusterIP:80). |
|
||||||
|
| `targetPort: 80` | The port on the **pod/container** traffic is forwarded to — Apache's port 80. |
|
||||||
|
| `nodePort: 30080` | The port opened on **each node's** IP for external access. |
|
||||||
|
|
||||||
|
So the path is: `<node-ip>:30080` → Service `:80` → pod `:80`. `30080` is inside the valid
|
||||||
|
NodePort range (`30000–32767`), so the API accepts it.
|
||||||
|
|
||||||
|
### Why all three ports are set explicitly
|
||||||
|
|
||||||
|
`targetPort` defaults to `port` if omitted (both 80 here, so it'd work either way), and
|
||||||
|
`nodePort` would be auto-assigned from the range if omitted — but the task pins it to `30080`,
|
||||||
|
so it's set explicitly. Being explicit also makes the manifest self-documenting.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Service created as NodePort with the right ports
|
||||||
|
kubectl get service httpd-service
|
||||||
|
|
||||||
|
# Endpoints populated = selector matched the ReplicaSet's pods (this is the key check)
|
||||||
|
kubectl get endpoints httpd-service
|
||||||
|
|
||||||
|
# Confirm it reaches the app (from a node or the jump-host)
|
||||||
|
curl -s http://<node-ip>:30080 | head -n 5
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — `httpd-service` of type `NodePort` showing `80:30080/TCP`, and
|
||||||
|
`kubectl get endpoints httpd-service` listing one IP per matching pod. If endpoints is empty,
|
||||||
|
the selector doesn't match the pods' labels — recheck them with
|
||||||
|
`kubectl get pods --show-labels`.
|
||||||
132
kubernetes/level 1/task-14.md
Normal file
132
kubernetes/level 1/task-14.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
We encountered an issue with our Nginx and PHP-FPM setup on the Kubernetes cluster this morning, which halted its functionality. Investigate and rectify the issue:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
The pod name is nginx-phpfpm and configmap name is nginx-config. Identify and fix the problem.
|
||||||
|
|
||||||
|
|
||||||
|
Once resolved, copy /home/thor/index.php file from the jump host to the nginx-container within the nginx document root. After this, you should be able to access the website using Website button on the top bar.
|
||||||
|
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Troubleshoot nginx + php-fpm — `nginx-phpfpm` (confirmed fix)
|
||||||
|
|
||||||
|
## Root cause (confirmed from the live spec)
|
||||||
|
|
||||||
|
The nginx config and the two container mounts don't agree on the document root:
|
||||||
|
|
||||||
|
| Source | Path |
|
||||||
|
|--------|------|
|
||||||
|
| nginx config `root` | `/var/www/html` |
|
||||||
|
| `nginx-container` shared-files mount | `/var/www/html` ✓ |
|
||||||
|
| `php-fpm-container` shared-files mount | `/usr/share/nginx/html` ✗ **mismatch** |
|
||||||
|
|
||||||
|
nginx forwards PHP requests to php-fpm with
|
||||||
|
`SCRIPT_FILENAME = $document_root$fastcgi_script_name` → `/var/www/html/index.php`. But in the
|
||||||
|
**php-fpm** container the shared volume is mounted at `/usr/share/nginx/html`, so the path
|
||||||
|
`/var/www/html/index.php` doesn't exist there and php-fpm returns **"File not found."** The two
|
||||||
|
containers share the same `emptyDir` volume, but at **different paths**, so they aren't actually
|
||||||
|
sharing the document root.
|
||||||
|
|
||||||
|
**Fix:** change the php-fpm container's `shared-files` mountPath from `/usr/share/nginx/html`
|
||||||
|
to `/var/www/html`, so both containers — and the nginx `root`, and the FastCGI
|
||||||
|
`SCRIPT_FILENAME` — all reference the same path.
|
||||||
|
|
||||||
|
## Step 1 — Recreate the pod with the corrected mountPath
|
||||||
|
|
||||||
|
A pod's `volumeMounts` are immutable in place, so delete and re-create. This heredoc is your
|
||||||
|
live spec (from `last-applied-configuration`) with **only** the php-fpm mountPath fixed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl delete pod nginx-phpfpm
|
||||||
|
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: nginx-phpfpm
|
||||||
|
namespace: default
|
||||||
|
labels:
|
||||||
|
app: php-app
|
||||||
|
spec:
|
||||||
|
volumes:
|
||||||
|
- name: shared-files
|
||||||
|
emptyDir: {}
|
||||||
|
- name: nginx-config-volume
|
||||||
|
configMap:
|
||||||
|
name: nginx-config
|
||||||
|
containers:
|
||||||
|
- name: php-fpm-container
|
||||||
|
image: php:7.2-fpm-alpine
|
||||||
|
volumeMounts:
|
||||||
|
- name: shared-files
|
||||||
|
mountPath: /var/www/html # FIXED: was /usr/share/nginx/html
|
||||||
|
- name: nginx-container
|
||||||
|
image: nginx:latest
|
||||||
|
volumeMounts:
|
||||||
|
- name: shared-files
|
||||||
|
mountPath: /var/www/html
|
||||||
|
- name: nginx-config-volume
|
||||||
|
mountPath: /etc/nginx/nginx.conf
|
||||||
|
subPath: nginx.conf
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2 — Copy the PHP file into the shared document root
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl cp /home/thor/index.php nginx-phpfpm:/var/www/html/index.php -c nginx-container
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy into `/var/www/html` (the now-consistent root) via the nginx container. Because the volume
|
||||||
|
is shared at the same path in both containers, php-fpm sees the file too and can execute it.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### Why the mismatch broke it even though the pod was "Running"
|
||||||
|
|
||||||
|
Both containers were healthy (`2/2 Running`) — this wasn't a crash. The failure was purely at
|
||||||
|
request time: nginx served on port 8099, matched `.php`, and handed php-fpm an absolute path
|
||||||
|
(`/var/www/html/...`) that was valid in nginx's filesystem but pointed at nothing in php-fpm's.
|
||||||
|
FastCGI passes a **path string**, not a file handle, so both containers must resolve that same
|
||||||
|
absolute path to the same bytes — which only happens if the shared volume is mounted at the
|
||||||
|
identical path in each. Aligning php-fpm's mount to `/var/www/html` closes that gap.
|
||||||
|
|
||||||
|
### Why copy after recreating
|
||||||
|
|
||||||
|
Recreating the pod starts with a fresh empty `shared-files` volume, so `index.php` is copied
|
||||||
|
**after** the pod is Running. It then lives in the shared root for the pod's lifetime and is
|
||||||
|
visible to both containers.
|
||||||
|
|
||||||
|
### Note on the listen port
|
||||||
|
|
||||||
|
The nginx config listens on **8099** (not 80), with `root /var/www/html`. Any Service exposing
|
||||||
|
this app must target port 8099 — the Website button relies on that mapping. This task doesn't
|
||||||
|
ask you to change the Service, just to fix the pod and drop in the file.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pod healthy after recreate
|
||||||
|
kubectl get pod nginx-phpfpm
|
||||||
|
|
||||||
|
# File present in the shared root, visible from BOTH containers
|
||||||
|
kubectl exec nginx-phpfpm -c nginx-container -- ls -l /var/www/html/index.php
|
||||||
|
kubectl exec nginx-phpfpm -c php-fpm-container -- ls -l /var/www/html/index.php
|
||||||
|
|
||||||
|
# App renders the PHP page (nginx listens on 8099)
|
||||||
|
kubectl exec nginx-phpfpm -c nginx-container -- curl -s http://localhost:8099/index.php | head
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — `nginx-phpfpm` `READY 2/2`, `index.php` visible from **both** containers under
|
||||||
|
`/var/www/html`, and the curl returning the rendered page. The **Website** button then loads the
|
||||||
|
site.
|
||||||
|
|
||||||
|
> The key sign the fix worked: `ls` succeeds from the **php-fpm** container too. Before the fix
|
||||||
|
> it would only appear under `/usr/share/nginx/html` there, which is exactly why php-fpm
|
||||||
|
> couldn't find it at `/var/www/html`.
|
||||||
100
kubernetes/level 1/task-2.md
Normal file
100
kubernetes/level 1/task-2.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
The Nautilus DevOps team is delving into Kubernetes for app management. One team member needs to create a deployment following these details:
|
||||||
|
|
||||||
|
|
||||||
|
Create a deployment named httpd to deploy the application httpd using the image httpd:latest (ensure to specify the tag)
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Kubernetes Deployment — `httpd`
|
||||||
|
|
||||||
|
A Deployment named `httpd` running the `httpd:latest` image. Applied inline via a
|
||||||
|
heredoc — no manifest file on disk.
|
||||||
|
|
||||||
|
## Apply (heredoc → kubectl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: httpd
|
||||||
|
labels:
|
||||||
|
app: httpd
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: httpd
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: httpd
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: httpd
|
||||||
|
image: httpd:latest
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The heredoc apply pattern
|
||||||
|
|
||||||
|
- **`kubectl apply -f -`** reads the manifest from **stdin**; the heredoc feeds the YAML
|
||||||
|
straight in, so nothing is written to disk.
|
||||||
|
- **`<<'EOF'` (delimiter quoted)** disables shell expansion inside the body, so any
|
||||||
|
`$VAR` / backticks in a manifest stay literal — the right default for Kubernetes YAML.
|
||||||
|
- **Indentation matters** — keep it exact, spaces not tabs.
|
||||||
|
|
||||||
|
### The manifest, field by field
|
||||||
|
|
||||||
|
- **`apiVersion: apps/v1` / `kind: Deployment`** — a Deployment is an `apps/v1` object
|
||||||
|
that manages a ReplicaSet, which in turn manages Pods. It gives you declarative
|
||||||
|
updates, rollouts/rollbacks, and self-healing (recreates pods that die).
|
||||||
|
- **`metadata.name: httpd`** — the Deployment's name, exactly as required.
|
||||||
|
- **`spec.replicas: 1`** — how many pod copies to run. The task doesn't specify a
|
||||||
|
count, so one replica is the minimal correct choice.
|
||||||
|
- **`spec.selector.matchLabels.app: httpd`** — how the Deployment finds the pods it
|
||||||
|
owns. This **must** match the pod template's labels below, or the API rejects the
|
||||||
|
manifest (`selector does not match template labels`).
|
||||||
|
- **`spec.template`** — the pod blueprint the Deployment stamps out:
|
||||||
|
- **`template.metadata.labels.app: httpd`** — labels applied to each pod; these are
|
||||||
|
what the selector targets. The label key/value is arbitrary but must be consistent
|
||||||
|
between selector and template.
|
||||||
|
- **`template.spec.containers`** — one container:
|
||||||
|
- **`name: httpd`** — container name.
|
||||||
|
- **`image: httpd:latest`** — the Apache HTTP Server image at the `latest` tag,
|
||||||
|
stated explicitly as required. (Bare `httpd` defaults to `latest`, but the task
|
||||||
|
asks for the tag to be specified.)
|
||||||
|
|
||||||
|
### Why the selector/template labels must agree
|
||||||
|
|
||||||
|
A Deployment is decoupled from its pods — it tracks them purely by label selector.
|
||||||
|
`spec.selector.matchLabels` is the query; `spec.template.metadata.labels` is what gets
|
||||||
|
stamped on each pod. If they don't match, the Deployment couldn't recognize its own
|
||||||
|
pods, so Kubernetes rejects the config outright. Keeping both `app: httpd` satisfies
|
||||||
|
that contract.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Deployment exists and is available
|
||||||
|
kubectl get deployment httpd
|
||||||
|
|
||||||
|
# Rollout completed
|
||||||
|
kubectl rollout status deployment/httpd
|
||||||
|
|
||||||
|
# Pod is running with the right image
|
||||||
|
kubectl get pods -l app=httpd \
|
||||||
|
-o jsonpath='{.items[0].spec.containers[0].image}{"\n"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — `httpd` deployment showing `READY 1/1`, rollout reporting success, and the
|
||||||
|
image printing `httpd:latest`.
|
||||||
|
|
||||||
|
> If a pod sits in `ImagePullBackOff`, the node may be offline or rate-limited on the
|
||||||
|
> pull. `kubectl describe pod <name>` shows pull events under `Events:`.
|
||||||
99
kubernetes/level 1/task-3.md
Normal file
99
kubernetes/level 1/task-3.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
The Nautilus DevOps team is planning to deploy some micro services on Kubernetes platform. The team has already set up a Kubernetes cluster and now they want to set up some namespaces, deployments etc. Based on the current requirements, the team has shared some details as below:
|
||||||
|
|
||||||
|
|
||||||
|
Create a namespace named dev and deploy a POD within it. Name the pod dev-nginx-pod and use the nginx image with the latest tag. Ensure to specify the tag as nginx:latest.
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Kubernetes Namespace + Pod — `dev` / `dev-nginx-pod`
|
||||||
|
|
||||||
|
Create a `dev` namespace and run `dev-nginx-pod` (image `nginx:latest`) inside it. Both
|
||||||
|
objects are applied in one heredoc using a multi-document manifest — no file on disk.
|
||||||
|
|
||||||
|
## Apply (heredoc → kubectl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: dev
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: dev-nginx-pod
|
||||||
|
namespace: dev
|
||||||
|
labels:
|
||||||
|
app: dev-nginx
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: nginx-container
|
||||||
|
image: nginx:latest
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The heredoc apply pattern
|
||||||
|
|
||||||
|
- **`kubectl apply -f -`** reads from **stdin**; the heredoc feeds the YAML straight in,
|
||||||
|
nothing written to disk.
|
||||||
|
- **`<<'EOF'` (delimiter quoted)** disables shell expansion, so any `$VAR`/backticks in
|
||||||
|
a manifest stay literal — the right default for Kubernetes YAML.
|
||||||
|
- **`---`** separates the two YAML documents in a single stream. `kubectl apply`
|
||||||
|
processes each in order, so the **Namespace is created before the Pod** that targets
|
||||||
|
it. Ordering matters: a pod referencing a namespace that doesn't yet exist would fail
|
||||||
|
with `namespaces "dev" not found`.
|
||||||
|
|
||||||
|
### The Namespace
|
||||||
|
|
||||||
|
- **`kind: Namespace` / `metadata.name: dev`** — a namespace is a virtual cluster
|
||||||
|
partition for isolating and grouping resources. Creating it first gives the pod
|
||||||
|
somewhere to live.
|
||||||
|
|
||||||
|
### The Pod
|
||||||
|
|
||||||
|
- **`metadata.name: dev-nginx-pod`** — the pod's name, exactly as required.
|
||||||
|
- **`metadata.namespace: dev`** — places the pod **in the `dev` namespace**. This is the
|
||||||
|
key field for the task: without it, the pod would land in `default`. (Equivalent to
|
||||||
|
passing `-n dev` on the command line, but pinning it in the manifest is explicit and
|
||||||
|
self-contained.)
|
||||||
|
- **`metadata.labels.app: dev-nginx`** — an optional label for selection/grouping. Not
|
||||||
|
required by the task, but good practice.
|
||||||
|
- **`spec.containers`** — one container:
|
||||||
|
- **`name: nginx-container`** — the container name.
|
||||||
|
- **`image: nginx:latest`** — the NGINX image at the `latest` tag, stated explicitly
|
||||||
|
as required. (Bare `nginx` defaults to `latest`, but the task asks for the tag to be
|
||||||
|
specified.)
|
||||||
|
|
||||||
|
### Namespace-scoping recap
|
||||||
|
|
||||||
|
The `namespace: dev` line in the pod's metadata is what satisfies "deploy a POD within
|
||||||
|
it." Everything else about the pod is standard; the namespace field is the one that
|
||||||
|
ties the two objects together.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Namespace exists
|
||||||
|
kubectl get namespace dev
|
||||||
|
|
||||||
|
# Pod is running inside the dev namespace
|
||||||
|
kubectl get pod dev-nginx-pod -n dev -o wide
|
||||||
|
|
||||||
|
# Image is correct
|
||||||
|
kubectl get pod dev-nginx-pod -n dev \
|
||||||
|
-o jsonpath='{.spec.containers[0].image}{"\n"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — namespace `dev` present, `dev-nginx-pod` in `Running` status within it, and
|
||||||
|
the image printing `nginx:latest`.
|
||||||
|
|
||||||
|
> Remember to pass `-n dev` on any `kubectl` command targeting this pod — it's not in
|
||||||
|
> the `default` namespace. If the pod sits in `ImagePullBackOff`, run
|
||||||
|
> `kubectl describe pod dev-nginx-pod -n dev` to see the pull events.
|
||||||
108
kubernetes/level 1/task-4.md
Normal file
108
kubernetes/level 1/task-4.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
The Nautilus DevOps team has noticed performance issues in some Kubernetes-hosted applications due to resource constraints. To address this, they plan to set limits on resource utilization. Here are the details:
|
||||||
|
|
||||||
|
|
||||||
|
Create a pod named httpd-pod with a container named httpd-container. Use the httpd image with the latest tag (specify as httpd:latest). Configure the following container-level resource requests and limits for the container:
|
||||||
|
|
||||||
|
Requests: Memory: 15Mi, CPU: 100m
|
||||||
|
|
||||||
|
Limits: Memory: 20Mi, CPU: 100m
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Kubernetes Pod with Resource Limits — `httpd-pod`
|
||||||
|
|
||||||
|
A pod running `httpd:latest` with container-level CPU/memory requests and limits.
|
||||||
|
Applied inline via a heredoc — no manifest file on disk.
|
||||||
|
|
||||||
|
## Apply (heredoc → kubectl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: httpd-pod
|
||||||
|
labels:
|
||||||
|
app: httpd
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: httpd-container
|
||||||
|
image: httpd:latest
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "15Mi"
|
||||||
|
cpu: "100m"
|
||||||
|
limits:
|
||||||
|
memory: "20Mi"
|
||||||
|
cpu: "100m"
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The heredoc apply pattern
|
||||||
|
|
||||||
|
- **`kubectl apply -f -`** reads from **stdin**; the heredoc feeds the YAML in, nothing
|
||||||
|
on disk.
|
||||||
|
- **`<<'EOF'` (delimiter quoted)** disables shell expansion, keeping `$VAR`/backticks
|
||||||
|
literal — the right default for Kubernetes YAML.
|
||||||
|
|
||||||
|
### The pod
|
||||||
|
|
||||||
|
- **`metadata.name: httpd-pod`** and container **`name: httpd-container`** — exactly as
|
||||||
|
required.
|
||||||
|
- **`image: httpd:latest`** — Apache HTTP Server at the `latest` tag, stated explicitly.
|
||||||
|
|
||||||
|
### The `resources` block — the heart of this task
|
||||||
|
|
||||||
|
Resource controls are set **per container**, under `spec.containers[].resources`:
|
||||||
|
|
||||||
|
- **`requests`** — the amount the scheduler **reserves** for the container. Kubernetes
|
||||||
|
places the pod on a node that has at least this much free, and the request is what
|
||||||
|
counts against node allocatable capacity. Here: `memory: 15Mi`, `cpu: 100m`.
|
||||||
|
- **`limits`** — the hard **ceiling** the container may use at runtime. Here:
|
||||||
|
`memory: 20Mi`, `cpu: 100m`.
|
||||||
|
|
||||||
|
The two behave very differently when exceeded:
|
||||||
|
|
||||||
|
| Resource | Over the limit → |
|
||||||
|
|----------|------------------|
|
||||||
|
| **CPU** | **Throttled** — the container is capped at its CPU limit; it's slowed, never killed. |
|
||||||
|
| **Memory** | **OOM-killed** — memory is incompressible, so exceeding the limit terminates the container (it restarts per its policy). |
|
||||||
|
|
||||||
|
### Units explained
|
||||||
|
|
||||||
|
- **`100m` CPU** = 100 millicores = 0.1 of a vCPU. CPU is measured in millicores; `1000m`
|
||||||
|
= 1 full core. Request and limit are both `100m` here, so the container is guaranteed
|
||||||
|
0.1 core and capped at 0.1 core.
|
||||||
|
- **`15Mi` / `20Mi` memory** — `Mi` is **mebibytes** (base-2, 1 Mi = 1,048,576 bytes),
|
||||||
|
distinct from `M` (megabytes, base-10, 1,000,000 bytes). The task specifies `Mi`, so
|
||||||
|
use `Mi` exactly — mixing units is a common mistake.
|
||||||
|
|
||||||
|
### Requests ≤ limits
|
||||||
|
|
||||||
|
Note memory request (`15Mi`) is below its limit (`20Mi`), while CPU request equals its
|
||||||
|
limit (`100m`). That's valid: a request must never exceed its limit, but being lower is
|
||||||
|
fine and normal — it lets the container burst up to the limit when the node has spare
|
||||||
|
capacity while only reserving the smaller request amount.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pod running
|
||||||
|
kubectl get pod httpd-pod
|
||||||
|
|
||||||
|
# Requests and limits are set correctly
|
||||||
|
kubectl get pod httpd-pod \
|
||||||
|
-o jsonpath='{.spec.containers[0].resources}{"\n"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — `httpd-pod` in `Running` status, and the resources printing requests
|
||||||
|
`{cpu:100m, memory:15Mi}` with limits `{cpu:100m, memory:20Mi}`.
|
||||||
|
|
||||||
|
> If the pod is `OOMKilled` on start, `httpd:latest` needed more than the `20Mi` memory
|
||||||
|
> limit — but Apache's base footprint fits comfortably, so the given values are fine.
|
||||||
132
kubernetes/level 1/task-5.md
Normal file
132
kubernetes/level 1/task-5.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
An application currently running on the Kubernetes cluster employs the nginx web server. The Nautilus application development team has introduced some recent changes that need deployment. They've crafted an image nginx:1.17 with the latest updates.
|
||||||
|
|
||||||
|
|
||||||
|
Execute a rolling update for this application, integrating the nginx:1.17 image. The deployment is named nginx-deployment.
|
||||||
|
|
||||||
|
Ensure all pods are operational post-update.
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Rolling Update — `nginx-deployment` → `nginx:1.17`
|
||||||
|
|
||||||
|
Update an existing deployment's image to `nginx:1.17` via a rolling update, then confirm
|
||||||
|
all pods are healthy.
|
||||||
|
|
||||||
|
## Primary method — `kubectl set image` (recommended)
|
||||||
|
|
||||||
|
Because the deployment already exists and you don't need its full manifest to change one
|
||||||
|
image, this is the canonical rolling-update command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Trigger the rolling update
|
||||||
|
kubectl set image deployment/nginx-deployment nginx=nginx:1.17
|
||||||
|
|
||||||
|
# Watch it roll out and block until complete
|
||||||
|
kubectl rollout status deployment/nginx-deployment
|
||||||
|
```
|
||||||
|
|
||||||
|
`nginx=nginx:1.17` means "set the container **named `nginx`** to image `nginx:1.17`."
|
||||||
|
Confirm the container's actual name first if unsure:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get deployment nginx-deployment \
|
||||||
|
-o jsonpath='{.spec.template.spec.containers[*].name}{"\n"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Use whatever name that prints on the left side of the `=`.
|
||||||
|
|
||||||
|
## Alternative — heredoc `apply`
|
||||||
|
|
||||||
|
You can also drive the update declaratively by re-applying the deployment with the new
|
||||||
|
image. **Caveat:** `apply` reconciles the whole pod template, so the manifest must match
|
||||||
|
the existing deployment's container name, labels, and selector — otherwise you'll change
|
||||||
|
more than intended. Grab the current spec first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get deployment nginx-deployment -o yaml # note container name, labels, replicas
|
||||||
|
```
|
||||||
|
|
||||||
|
Then apply with the image bumped to `nginx:1.17` (adjust names/replicas to match what you
|
||||||
|
saw):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: nginx-deployment
|
||||||
|
labels:
|
||||||
|
app: nginx
|
||||||
|
spec:
|
||||||
|
replicas: 3
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: nginx
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: nginx
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: nginx
|
||||||
|
image: nginx:1.17
|
||||||
|
EOF
|
||||||
|
|
||||||
|
kubectl rollout status deployment/nginx-deployment
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### What a rolling update does
|
||||||
|
|
||||||
|
A Deployment's default strategy is `RollingUpdate`: when the pod template changes (here,
|
||||||
|
the image), the Deployment creates a **new ReplicaSet** for `nginx:1.17` and shifts pods
|
||||||
|
over **incrementally** — spinning up new pods and tearing down old ones a few at a time,
|
||||||
|
governed by `maxSurge` (how many extra pods above desired count are allowed) and
|
||||||
|
`maxUnavailable` (how many below). This keeps the app serving throughout, with no full
|
||||||
|
outage. Changing the image is exactly the kind of template change that triggers it.
|
||||||
|
|
||||||
|
### `set image` vs. heredoc `apply`
|
||||||
|
|
||||||
|
- **`set image`** patches just the container image on the live object. It's surgical,
|
||||||
|
needs no knowledge of the rest of the spec, and is the idiomatic way to roll a new
|
||||||
|
image onto an existing deployment. Preferred when you're changing one field.
|
||||||
|
- **`apply`** reconciles the entire manifest you feed it against the live object. It's the
|
||||||
|
right tool when you own the manifest as source of truth, but for a one-field image bump
|
||||||
|
on a deployment you didn't author, it forces you to reproduce the full spec correctly —
|
||||||
|
more surface area to get wrong. That's why `set image` is primary here.
|
||||||
|
|
||||||
|
Both produce the same underlying rolling update; they differ only in how the change is
|
||||||
|
expressed.
|
||||||
|
|
||||||
|
### `rollout status`
|
||||||
|
|
||||||
|
`kubectl rollout status` blocks until the new ReplicaSet is fully rolled out and all
|
||||||
|
updated pods report Ready — the "ensure all pods are operational post-update" requirement.
|
||||||
|
If the update stalls (e.g. a bad image), it surfaces there rather than silently leaving
|
||||||
|
old pods running.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All pods updated and running
|
||||||
|
kubectl get pods -l app=nginx -o wide
|
||||||
|
|
||||||
|
# Every pod now on nginx:1.17
|
||||||
|
kubectl get deployment nginx-deployment \
|
||||||
|
-o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
|
||||||
|
|
||||||
|
# Rollout history (shows the new revision)
|
||||||
|
kubectl rollout history deployment/nginx-deployment
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — all pods `Running` and `READY`, the image printing `nginx:1.17`, and a new
|
||||||
|
revision recorded in the rollout history.
|
||||||
|
|
||||||
|
> If the rollout hangs with new pods in `ImagePullBackOff`, the `nginx:1.17` pull failed
|
||||||
|
> (node offline / rate-limited). `kubectl describe pod <name>` shows the cause; you can
|
||||||
|
> revert with `kubectl rollout undo deployment/nginx-deployment`.
|
||||||
85
kubernetes/level 1/task-6.md
Normal file
85
kubernetes/level 1/task-6.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
Earlier today, the Nautilus DevOps team deployed a new release for an application. However, a customer has reported a bug related to this recent release. Consequently, the team aims to revert to the previous version.
|
||||||
|
|
||||||
|
|
||||||
|
There exists a deployment named nginx-deployment; initiate a rollback to the previous revision.
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Rollback — `nginx-deployment` to the previous revision
|
||||||
|
|
||||||
|
Revert the deployment to the revision that was live before the buggy release.
|
||||||
|
|
||||||
|
> **Why no heredoc `apply` here:** a rollback isn't a declarative manifest operation. It
|
||||||
|
> doesn't describe a desired end-state in YAML — it tells the Deployment controller to
|
||||||
|
> switch back to a **previously recorded ReplicaSet revision** stored in rollout history.
|
||||||
|
> That's an imperative rollout action, so `kubectl rollout undo` is the correct (and only
|
||||||
|
> idiomatic) tool. Re-applying an old manifest would work only if you happened to still
|
||||||
|
> have the exact prior YAML, and even then it's the wrong abstraction.
|
||||||
|
|
||||||
|
## Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Roll back to the immediately previous revision
|
||||||
|
kubectl rollout undo deployment/nginx-deployment
|
||||||
|
|
||||||
|
# Block until the rollback finishes and pods are Ready
|
||||||
|
kubectl rollout status deployment/nginx-deployment
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### Rollout history and revisions
|
||||||
|
|
||||||
|
Every change to a Deployment's pod template (image bump, env change, etc.) creates a new
|
||||||
|
**revision**, each backed by its own ReplicaSet. Kubernetes retains old ReplicaSets (up to
|
||||||
|
`spec.revisionHistoryLimit`, default 10) precisely so you can roll back. You can inspect
|
||||||
|
them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl rollout history deployment/nginx-deployment
|
||||||
|
```
|
||||||
|
|
||||||
|
### What `rollout undo` does
|
||||||
|
|
||||||
|
- With no `--to-revision` flag, it reverts to the **immediately previous** revision — the
|
||||||
|
one running before the current (buggy) release. That's exactly this task.
|
||||||
|
- Mechanically, it scales the **previous ReplicaSet** back up and the **current** one down
|
||||||
|
using the same `RollingUpdate` strategy — so the rollback itself is gradual and keeps the
|
||||||
|
app serving, no full outage.
|
||||||
|
- The rollback is recorded as a **new** revision in history (revisions move forward even
|
||||||
|
when the content is an older template), so you always have a clean audit trail.
|
||||||
|
|
||||||
|
### Targeting a specific revision (if needed)
|
||||||
|
|
||||||
|
If "previous" isn't the right target, pick an explicit revision from the history:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl rollout undo deployment/nginx-deployment --to-revision=<N>
|
||||||
|
```
|
||||||
|
|
||||||
|
For this task the default (previous revision) is what's required, so no flag is needed.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Rollback completed, pods Ready
|
||||||
|
kubectl rollout status deployment/nginx-deployment
|
||||||
|
|
||||||
|
# Confirm the image/template reverted to the prior version
|
||||||
|
kubectl get deployment nginx-deployment \
|
||||||
|
-o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
|
||||||
|
|
||||||
|
# History shows a new revision at the top reflecting the rollback
|
||||||
|
kubectl rollout history deployment/nginx-deployment
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — rollout reports success with all pods `Running`/`READY`, the container image
|
||||||
|
reverted to the previous release's tag, and a new revision entry recording the rollback.
|
||||||
|
|
||||||
|
> If the rollback stalls, `kubectl describe deployment nginx-deployment` and
|
||||||
|
> `kubectl get pods -l <selector>` show what's blocking (e.g. the old image also failing to
|
||||||
|
> pull).
|
||||||
112
kubernetes/level 1/task-7.md
Normal file
112
kubernetes/level 1/task-7.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# Assignment
|
||||||
|
The Nautilus DevOps team is gearing up to deploy applications on a Kubernetes cluster for migration purposes. A team member has been tasked with creating a ReplicaSet outlined below:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Create a ReplicaSet using httpd image with latest tag (ensure to specify as httpd:latest) and name it httpd-replicaset.
|
||||||
|
|
||||||
|
|
||||||
|
Apply labels: app as httpd_app, type as front-end.
|
||||||
|
|
||||||
|
|
||||||
|
Name the container httpd-container. Ensure the replica count is 4.
|
||||||
|
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Kubernetes ReplicaSet — `httpd-replicaset`
|
||||||
|
|
||||||
|
A ReplicaSet maintaining 4 `httpd:latest` pods, labeled `app: httpd_app` and
|
||||||
|
`type: front-end`. Applied inline via a heredoc — no manifest file on disk.
|
||||||
|
|
||||||
|
## Apply (heredoc → kubectl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: ReplicaSet
|
||||||
|
metadata:
|
||||||
|
name: httpd-replicaset
|
||||||
|
labels:
|
||||||
|
app: httpd_app
|
||||||
|
type: front-end
|
||||||
|
spec:
|
||||||
|
replicas: 4
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: httpd_app
|
||||||
|
type: front-end
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: httpd_app
|
||||||
|
type: front-end
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: httpd-container
|
||||||
|
image: httpd:latest
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The heredoc apply pattern
|
||||||
|
|
||||||
|
- **`kubectl apply -f -`** reads from **stdin**; the heredoc feeds the YAML in, nothing on
|
||||||
|
disk.
|
||||||
|
- **`<<'EOF'` (delimiter quoted)** disables shell expansion, keeping `$VAR`/backticks
|
||||||
|
literal — the right default for Kubernetes YAML.
|
||||||
|
|
||||||
|
### The manifest, field by field
|
||||||
|
|
||||||
|
- **`apiVersion: apps/v1` / `kind: ReplicaSet`** — a ReplicaSet ensures a specified number
|
||||||
|
of identical pod replicas are running at all times, recreating any that die. (In
|
||||||
|
practice you'd usually use a Deployment, which manages ReplicaSets and adds rollouts —
|
||||||
|
but the task asks for a bare ReplicaSet.)
|
||||||
|
- **`metadata.name: httpd-replicaset`** — the RS name, exactly as required.
|
||||||
|
- **`metadata.labels`** — `app: httpd_app` and `type: front-end` on the ReplicaSet object
|
||||||
|
itself, as required.
|
||||||
|
- **`spec.replicas: 4`** — the desired pod count. The RS controller works continuously to
|
||||||
|
keep exactly 4 matching pods running.
|
||||||
|
- **`spec.selector.matchLabels`** — how the RS identifies the pods it owns. This **must**
|
||||||
|
match the pod template's labels, or the API rejects the manifest with `selector does not
|
||||||
|
match template labels`.
|
||||||
|
- **`spec.template`** — the pod blueprint:
|
||||||
|
- **`template.metadata.labels`** — `app: httpd_app`, `type: front-end` stamped on every
|
||||||
|
pod. These are what the selector targets.
|
||||||
|
- **`template.spec.containers`** — one container: **`name: httpd-container`** with
|
||||||
|
**`image: httpd:latest`** (tag stated explicitly, as required).
|
||||||
|
|
||||||
|
### Why the selector and template labels must agree
|
||||||
|
|
||||||
|
A ReplicaSet is decoupled from its pods and tracks them purely by label selector.
|
||||||
|
`spec.selector.matchLabels` is the query; `spec.template.metadata.labels` is what gets
|
||||||
|
stamped on each pod. If they don't match, the RS couldn't recognize its own pods, so
|
||||||
|
Kubernetes rejects the config outright. Both carry `app: httpd_app` + `type: front-end`
|
||||||
|
to satisfy that contract.
|
||||||
|
|
||||||
|
> Selector caution: because a ReplicaSet adopts **any** existing pod matching its selector,
|
||||||
|
> reusing labels that other pods already carry can cause it to adopt (or fight over) them.
|
||||||
|
> The two-label selector here is specific enough to avoid that in a clean environment.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ReplicaSet exists with 4 ready replicas
|
||||||
|
kubectl get rs httpd-replicaset
|
||||||
|
|
||||||
|
# Pods are running and labeled
|
||||||
|
kubectl get pods -l app=httpd_app,type=front-end --show-labels
|
||||||
|
|
||||||
|
# Container image is correct
|
||||||
|
kubectl get rs httpd-replicaset \
|
||||||
|
-o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — `httpd-replicaset` showing `DESIRED 4 / CURRENT 4 / READY 4`, four pods
|
||||||
|
`Running` with both labels, and the image printing `httpd:latest`.
|
||||||
|
|
||||||
|
> If pods sit in `ImagePullBackOff`, the `httpd:latest` pull failed (node offline /
|
||||||
|
> rate-limited). `kubectl describe pod <name>` shows the pull events.
|
||||||
126
kubernetes/level 1/task-8.md
Normal file
126
kubernetes/level 1/task-8.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
The Nautilus DevOps team is setting up recurring tasks on different schedules. Currently, they're developing scripts to be executed periodically. To kickstart the process, they're creating cron jobs in the Kubernetes cluster with placeholder commands. Follow the instructions below:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Create a cronjob named datacenter.
|
||||||
|
|
||||||
|
|
||||||
|
Set Its schedule to something like */12 * * * *. You can set any schedule for now.
|
||||||
|
|
||||||
|
|
||||||
|
Name the container cron-datacenter.
|
||||||
|
|
||||||
|
|
||||||
|
Utilize the httpd image with latest tag (specify as httpd:latest).
|
||||||
|
|
||||||
|
|
||||||
|
Execute the dummy command echo Welcome to xfusioncorp!.
|
||||||
|
|
||||||
|
|
||||||
|
Ensure the restart policy is OnFailure.
|
||||||
|
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Kubernetes CronJob — `datacenter`
|
||||||
|
|
||||||
|
A CronJob that runs `echo Welcome to xfusioncorp!` in an `httpd:latest` container on a
|
||||||
|
schedule. Applied inline via a heredoc — no manifest file on disk.
|
||||||
|
|
||||||
|
## Apply (heredoc → kubectl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: CronJob
|
||||||
|
metadata:
|
||||||
|
name: datacenter
|
||||||
|
spec:
|
||||||
|
schedule: "*/12 * * * *"
|
||||||
|
jobTemplate:
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: cron-datacenter
|
||||||
|
image: httpd:latest
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- echo Welcome to xfusioncorp!
|
||||||
|
restartPolicy: OnFailure
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The heredoc apply pattern
|
||||||
|
|
||||||
|
- **`kubectl apply -f -`** reads from **stdin**; the heredoc feeds the YAML in, nothing on
|
||||||
|
disk.
|
||||||
|
- **`<<'EOF'` (delimiter quoted)** disables shell expansion, keeping `$VAR`/backticks
|
||||||
|
literal — important here so `echo Welcome to xfusioncorp!` is stored verbatim rather than
|
||||||
|
evaluated by the jump-host's shell.
|
||||||
|
|
||||||
|
### The nested structure — the part that trips people up
|
||||||
|
|
||||||
|
A CronJob wraps three levels deep. From outside in:
|
||||||
|
|
||||||
|
1. **`CronJob.spec`** — the schedule and how to spawn jobs.
|
||||||
|
2. **`jobTemplate.spec`** — the **Job** created on each tick.
|
||||||
|
3. **`template.spec`** — the **Pod** the Job runs.
|
||||||
|
|
||||||
|
So the container and restart policy live at `spec.jobTemplate.spec.template.spec`, not
|
||||||
|
directly under the CronJob. Getting the nesting wrong is the most common CronJob error.
|
||||||
|
|
||||||
|
### Field by field
|
||||||
|
|
||||||
|
- **`apiVersion: batch/v1` / `kind: CronJob`** — CronJob is a `batch/v1` object (stable
|
||||||
|
since Kubernetes 1.21; the old `batch/v1beta1` is removed in modern clusters — use
|
||||||
|
`batch/v1`).
|
||||||
|
- **`metadata.name: datacenter`** — the CronJob name, exactly as required.
|
||||||
|
- **`spec.schedule: "*/12 * * * *"`** — standard cron syntax
|
||||||
|
(minute hour day-of-month month day-of-week). `*/12 * * * *` = every 12 minutes. The task
|
||||||
|
allows any schedule; quote the string so YAML doesn't choke on the `*`.
|
||||||
|
- **`jobTemplate.spec.template.spec.containers`** — one container:
|
||||||
|
- **`name: cron-datacenter`** — the container name, exactly as required.
|
||||||
|
- **`image: httpd:latest`** — tag stated explicitly, as required.
|
||||||
|
- **`command`** — the dummy command. Written as `["/bin/sh","-c","echo Welcome to
|
||||||
|
xfusioncorp!"]` so the shell handles the phrase as one command. `command` overrides the
|
||||||
|
image's default entrypoint (httpd's web server), which is what we want — this is a
|
||||||
|
one-shot echo, not a running server.
|
||||||
|
- **`restartPolicy: OnFailure`** — required. It sits at the **pod** level (inside
|
||||||
|
`template.spec`), not on the container. For Jobs/CronJobs only `OnFailure` and `Never` are
|
||||||
|
valid (`Always` is rejected, since a batch job is meant to complete, not run forever).
|
||||||
|
`OnFailure` restarts the pod if the command exits non-zero.
|
||||||
|
|
||||||
|
### Why `command` uses `/bin/sh -c`
|
||||||
|
|
||||||
|
Passing the echo through `sh -c` runs it as a shell command, so the full phrase (with its
|
||||||
|
spaces and `!`) is handled correctly as a single string. Listing bare args instead would
|
||||||
|
also work for a simple echo, but the `sh -c` form is robust and the common pattern for
|
||||||
|
dummy commands.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# CronJob registered with the schedule
|
||||||
|
kubectl get cronjob datacenter
|
||||||
|
|
||||||
|
# After a scheduled tick, a Job (and its pod) appears
|
||||||
|
kubectl get jobs -l job-name --watch # Ctrl-C once one shows
|
||||||
|
kubectl get pods -l job-name
|
||||||
|
|
||||||
|
# Check the output of a completed run
|
||||||
|
kubectl logs job/<job-name-from-above>
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — `datacenter` listed with schedule `*/12 * * * *`; after a tick, a Job runs to
|
||||||
|
completion and its pod's logs show `Welcome to xfusioncorp!`.
|
||||||
|
|
||||||
|
> To trigger a run immediately instead of waiting for the schedule:
|
||||||
|
> `kubectl create job --from=cronjob/datacenter datacenter-manual`
|
||||||
107
kubernetes/level 1/task-9.md
Normal file
107
kubernetes/level 1/task-9.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
The Nautilus DevOps team is crafting jobs in the Kubernetes cluster. While they're developing actual scripts/commands, they're currently setting up templates and testing jobs with dummy commands. Please create a job template as per details given below:
|
||||||
|
|
||||||
|
|
||||||
|
Create a job named countdown-datacenter.
|
||||||
|
|
||||||
|
The spec template should be named countdown-datacenter (under metadata), and the container should be named container-countdown-datacenter
|
||||||
|
|
||||||
|
Utilize image ubuntu with latest tag (ensure to specify as ubuntu:latest), and set the restart policy to Never.
|
||||||
|
|
||||||
|
Execute the command sleep 5
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Kubernetes Job — `countdown-datacenter`
|
||||||
|
|
||||||
|
A one-shot Job that runs `sleep 5` in an `ubuntu:latest` container. Applied inline via a
|
||||||
|
heredoc — no manifest file on disk.
|
||||||
|
|
||||||
|
## Apply (heredoc → kubectl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: countdown-datacenter
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
name: countdown-datacenter
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: container-countdown-datacenter
|
||||||
|
image: ubuntu:latest
|
||||||
|
command:
|
||||||
|
- sleep
|
||||||
|
- "5"
|
||||||
|
restartPolicy: Never
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The heredoc apply pattern
|
||||||
|
|
||||||
|
- **`kubectl apply -f -`** reads from **stdin**; the heredoc feeds the YAML in, nothing on
|
||||||
|
disk.
|
||||||
|
- **`<<'EOF'` (delimiter quoted)** disables shell expansion, keeping the manifest literal —
|
||||||
|
the right default for Kubernetes YAML.
|
||||||
|
|
||||||
|
### Job vs. CronJob
|
||||||
|
|
||||||
|
A **Job** runs a pod to **completion** once (or a set number of times), unlike a CronJob
|
||||||
|
which spawns Jobs on a schedule. So there's no `schedule` or `jobTemplate` here — the pod
|
||||||
|
template sits directly under `spec.template`, one level shallower than a CronJob.
|
||||||
|
|
||||||
|
### Field by field
|
||||||
|
|
||||||
|
- **`apiVersion: batch/v1` / `kind: Job`** — Job is a `batch/v1` object.
|
||||||
|
- **`metadata.name: countdown-datacenter`** — the Job's name, exactly as required.
|
||||||
|
- **`spec.template.metadata.name: countdown-datacenter`** — the pod template name, as the
|
||||||
|
task explicitly requests. (Note: a template name is optional and normally ignored for
|
||||||
|
Jobs — the actual pod gets an auto-generated name like `countdown-datacenter-abcde` — but
|
||||||
|
the task asks for it, so it's set.)
|
||||||
|
- **`spec.template.spec.containers`** — one container:
|
||||||
|
- **`name: container-countdown-datacenter`** — the container name, exactly as required.
|
||||||
|
- **`image: ubuntu:latest`** — tag stated explicitly, as required.
|
||||||
|
- **`command: ["sleep", "5"]`** — the dummy command. The `"5"` is **quoted** so YAML
|
||||||
|
passes it as a string argument; command args must be strings, and an unquoted `5` would
|
||||||
|
be parsed as an integer and rejected.
|
||||||
|
- **`restartPolicy: Never`** — required. It sits at the **pod** level (inside
|
||||||
|
`template.spec`), not on the container. Jobs accept only `Never` or `OnFailure` (`Always`
|
||||||
|
is invalid for batch workloads). `Never` means if the pod fails, the Job creates a *new*
|
||||||
|
pod rather than restarting the existing one.
|
||||||
|
|
||||||
|
### `restartPolicy: Never` vs. `OnFailure` for Jobs
|
||||||
|
|
||||||
|
Both are valid for Jobs, and they differ in failure handling:
|
||||||
|
- **`Never`** — a failed pod is left as-is; the Job spins up a fresh pod for the next
|
||||||
|
attempt. You end up with visible failed pods (useful for debugging).
|
||||||
|
- **`OnFailure`** — the same pod's container is restarted in place.
|
||||||
|
|
||||||
|
The task specifies `Never`, so failed attempts (if any) appear as separate pods.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Job created and completing
|
||||||
|
kubectl get job countdown-datacenter
|
||||||
|
|
||||||
|
# Pod runs then completes
|
||||||
|
kubectl get pods -l job-name=countdown-datacenter
|
||||||
|
|
||||||
|
# Confirm image and command
|
||||||
|
kubectl get job countdown-datacenter \
|
||||||
|
-o jsonpath='{.spec.template.spec.containers[0].image}{" "}{.spec.template.spec.containers[0].command}{"\n"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — `countdown-datacenter` showing `COMPLETIONS 1/1` after ~5 seconds, its pod
|
||||||
|
transitioning `Running` → `Completed`, and the jsonpath printing `ubuntu:latest [sleep 5]`.
|
||||||
|
|
||||||
|
> The pod runs `sleep 5` then exits 0, so the Job completes cleanly. If it shows `Error`,
|
||||||
|
> check `kubectl logs job/countdown-datacenter`.
|
||||||
141
kubernetes/level 2/task-1.md
Normal file
141
kubernetes/level 2/task-1.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
We are working on an application that will be deployed on multiple containers within a pod on Kubernetes cluster. There is a requirement to share a volume among the containers to save some temporary data. The Nautilus DevOps team is developing a similar template to replicate the scenario. Below you can find more details about it.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Create a pod named volume-share-nautilus.
|
||||||
|
|
||||||
|
|
||||||
|
For the first container, use image ubuntu with latest tag only and remember to mention the tag i.e ubuntu:latest, container should be named as volume-container-nautilus-1, and run a sleep command for it so that it remains in running state. Volume volume-share should be mounted at path /tmp/beta.
|
||||||
|
|
||||||
|
|
||||||
|
For the second container, use image ubuntu with the latest tag only and remember to mention the tag i.e ubuntu:latest, container should be named as volume-container-nautilus-2, and again run a sleep command for it so that it remains in running state. Volume volume-share should be mounted at path /tmp/apps.
|
||||||
|
|
||||||
|
|
||||||
|
Volume name should be volume-share of type emptyDir.
|
||||||
|
|
||||||
|
|
||||||
|
After creating the pod, exec into the first container i.e volume-container-nautilus-1, and just for testing create a file beta.txt with the content Welcome to xFusionCorp Industries under the mounted path of first container i.e /tmp/beta.
|
||||||
|
|
||||||
|
|
||||||
|
The file beta.txt should be present under the mounted path /tmp/apps on the second container volume-container-nautilus-2 as well, since they are using a shared volume.
|
||||||
|
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Kubernetes Shared-Volume Pod — `volume-share-nautilus`
|
||||||
|
|
||||||
|
Two `ubuntu:latest` containers sharing one `emptyDir` volume mounted at different paths, then a
|
||||||
|
test proving a file written in one appears in the other. Applied inline via a heredoc — no
|
||||||
|
manifest file on disk.
|
||||||
|
|
||||||
|
## Step 1 — Apply the pod (heredoc → kubectl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: volume-share-nautilus
|
||||||
|
spec:
|
||||||
|
volumes:
|
||||||
|
- name: volume-share
|
||||||
|
emptyDir: {}
|
||||||
|
containers:
|
||||||
|
- name: volume-container-nautilus-1
|
||||||
|
image: ubuntu:latest
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- "sleep infinity"
|
||||||
|
volumeMounts:
|
||||||
|
- name: volume-share
|
||||||
|
mountPath: /tmp/beta
|
||||||
|
- name: volume-container-nautilus-2
|
||||||
|
image: ubuntu:latest
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- "sleep infinity"
|
||||||
|
volumeMounts:
|
||||||
|
- name: volume-share
|
||||||
|
mountPath: /tmp/apps
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2 — Write the test file in container 1
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl exec -it volume-share-nautilus -c volume-container-nautilus-1 -- \
|
||||||
|
bash -c 'echo "Welcome to xFusionCorp Industries" > /tmp/beta/beta.txt'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3 — Confirm it appears in container 2
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl exec -it volume-share-nautilus -c volume-container-nautilus-2 -- \
|
||||||
|
cat /tmp/apps/beta.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output: `Welcome to xFusionCorp Industries`
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The heredoc apply pattern
|
||||||
|
|
||||||
|
- **`kubectl apply -f -`** reads from **stdin**; nothing written to disk.
|
||||||
|
- **`<<'EOF'` (delimiter quoted)** keeps the manifest literal — the right default for k8s YAML.
|
||||||
|
|
||||||
|
### One volume, two mount points
|
||||||
|
|
||||||
|
The heart of this task: a **single** `emptyDir` volume (`volume-share`) declared once under
|
||||||
|
`spec.volumes`, then mounted into **both** containers via `volumeMounts` — but at **different
|
||||||
|
paths**:
|
||||||
|
|
||||||
|
- container 1 sees it at `/tmp/beta`
|
||||||
|
- container 2 sees it at `/tmp/apps`
|
||||||
|
|
||||||
|
Both `volumeMounts` reference the same volume `name: volume-share`, so they're two windows onto
|
||||||
|
the **same underlying storage**. A write through one window is instantly visible through the
|
||||||
|
other — which is why `beta.txt` created at `/tmp/beta/beta.txt` shows up at
|
||||||
|
`/tmp/apps/beta.txt`. The mount paths are just where each container chooses to see the shared
|
||||||
|
data; the bytes are shared.
|
||||||
|
|
||||||
|
### `emptyDir` — pod-lifetime shared scratch
|
||||||
|
|
||||||
|
- **`emptyDir: {}`** creates an empty directory when the pod is scheduled to a node, shared by
|
||||||
|
all containers in the pod, and deleted when the pod is removed. It's the standard choice for
|
||||||
|
ephemeral data shared between containers in a pod — exactly this "temporary data" scenario.
|
||||||
|
|
||||||
|
### Why the `sleep` command
|
||||||
|
|
||||||
|
`ubuntu:latest` has no long-running default process — it would start, find nothing to do, and
|
||||||
|
exit, crash-looping the pod. Giving each container `sleep infinity` (via `/bin/sh -c`) provides
|
||||||
|
a foreground process that never returns, so both containers stay **Running** and remain
|
||||||
|
available to `exec` into. `sleep infinity` is cleaner than a fixed duration (which would
|
||||||
|
eventually exit); `tail -f /dev/null` or a `while true` loop are equivalent.
|
||||||
|
|
||||||
|
### `kubectl exec -c` targets a specific container
|
||||||
|
|
||||||
|
Because the pod is multi-container, `kubectl exec` **requires** `-c <container>` to say which
|
||||||
|
one — without it, kubectl defaults to the first container but warns; being explicit avoids
|
||||||
|
ambiguity. Step 2 targets container 1 to write; Step 3 targets container 2 to read.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Both containers up
|
||||||
|
kubectl get pod volume-share-nautilus # READY should be 2/2
|
||||||
|
|
||||||
|
# The shared file, read from container 2's mount path
|
||||||
|
kubectl exec volume-share-nautilus -c volume-container-nautilus-2 -- cat /tmp/apps/beta.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — `volume-share-nautilus` `READY 2/2 Running`, and the `cat` returning
|
||||||
|
`Welcome to xFusionCorp Industries` from container 2's `/tmp/apps` path.
|
||||||
|
|
||||||
|
> If a container is in `CrashLoopBackOff`, its `sleep` command didn't take — confirm with
|
||||||
|
> `kubectl get pod volume-share-nautilus -o jsonpath='{.spec.containers[*].command}'`.
|
||||||
120
kubernetes/level 2/task-10.md
Normal file
120
kubernetes/level 2/task-10.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
Last week, the Nautilus DevOps team deployed a redis app on Kubernetes cluster, which was working fine so far. This morning one of the team members was making some changes in this existing setup, but he made some mistakes and the app went down. We need to fix this as soon as possible. Please take a look.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
The deployment name is redis-deployment. The pods are not in running state right now, so please look into the issue and fix the same.
|
||||||
|
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# - incorrect config map name
|
||||||
|
# - incorrect image on redis
|
||||||
|
```
|
||||||
|
|
||||||
|
# Troubleshoot Deployment — `redis-deployment`
|
||||||
|
|
||||||
|
The redis pods won't start after a bad edit. This walks the diagnosis, then the fix for whichever
|
||||||
|
root cause `describe` reveals.
|
||||||
|
|
||||||
|
## Step 1 — Diagnose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Deployment + pod state
|
||||||
|
kubectl get deployment redis-deployment
|
||||||
|
kubectl get pods -l app=redis # adjust selector if different
|
||||||
|
|
||||||
|
# The authoritative source: per-container state + Events at the bottom
|
||||||
|
kubectl describe pod -l app=redis
|
||||||
|
# (or describe a specific pod name from the get output)
|
||||||
|
|
||||||
|
# The full spec, to spot the bad edit
|
||||||
|
kubectl get deployment redis-deployment -o yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Read the **container state** and **Events** in `describe`. The status tells you the class of bug:
|
||||||
|
|
||||||
|
| Symptom in `describe` | Root cause |
|
||||||
|
|-----------------------|------------|
|
||||||
|
| `ErrImagePull` / `ImagePullBackOff` | **Image name/tag typo** (e.g. `redis:alpin` → `redis:alpine`). |
|
||||||
|
| `CreateContainerConfigError` | **Bad reference** — a `configMapKeyRef` / `configMap` volume / `secretKeyRef` pointing at a name or key that doesn't exist. |
|
||||||
|
| `CrashLoopBackOff` | Container starts then exits — bad command/args or a config the app rejects. |
|
||||||
|
| `Pending` / `FailedScheduling` | An unschedulable request (e.g. `resources.requests` too high, bad nodeSelector). |
|
||||||
|
|
||||||
|
## Step 2 — Fix (match to what you found)
|
||||||
|
|
||||||
|
### A) Image typo → correct the image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Confirm the container name and the wrong image
|
||||||
|
kubectl get deployment redis-deployment \
|
||||||
|
-o jsonpath='{.spec.template.spec.containers[0].name}{" "}{.spec.template.spec.containers[0].image}{"\n"}'
|
||||||
|
|
||||||
|
# Fix it (use the real container name = left of '='; correct the image/tag)
|
||||||
|
kubectl set image deployment/redis-deployment redis-container=redis:alpine
|
||||||
|
```
|
||||||
|
|
||||||
|
### B) Bad configMap / volume reference → correct the name
|
||||||
|
|
||||||
|
If a `configMap` volume or `configMapKeyRef` points at a mistyped name, fix it in place:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl edit deployment redis-deployment
|
||||||
|
# find the wrong reference (e.g. name: redis-cofig) and correct it (redis-config),
|
||||||
|
# matching an existing ConfigMap:
|
||||||
|
kubectl get configmaps
|
||||||
|
```
|
||||||
|
|
||||||
|
### C) Unschedulable resource request → lower it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example: a request of "2" CPU on a small node leaves the pod Pending
|
||||||
|
kubectl edit deployment redis-deployment
|
||||||
|
# correct spec.template.spec.containers[].resources.requests to a sane value
|
||||||
|
```
|
||||||
|
|
||||||
|
Any of these edits changes the pod template and triggers a fresh rollout automatically.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl rollout status deployment/redis-deployment
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### Why `describe` is the first move, not a guess
|
||||||
|
|
||||||
|
A broken pod advertises its exact failure in its container state and Events. `ImagePullBackOff`
|
||||||
|
names the image it couldn't pull (revealing a typo); `CreateContainerConfigError` names the
|
||||||
|
missing ConfigMap/Secret; `FailedScheduling` states why no node fits. Reading that first tells you
|
||||||
|
*which* field the "mistake" touched, so you fix one thing instead of shotgunning changes.
|
||||||
|
|
||||||
|
### Why in-place edits over re-apply here
|
||||||
|
|
||||||
|
The fix is a single-field correction on a deployment you didn't author and whose full spec you may
|
||||||
|
not have cleanly. `kubectl set image` (for the image) and `kubectl edit` (for a reference or
|
||||||
|
resource value) touch exactly the broken field and leave the rest intact — safer than
|
||||||
|
reconstructing the whole manifest. Each edit updates the pod template, so the Deployment rolls out
|
||||||
|
corrected pods on its own.
|
||||||
|
|
||||||
|
### Why the pods recover automatically
|
||||||
|
|
||||||
|
A Deployment continuously reconciles toward its spec. Once the template is valid (real image,
|
||||||
|
existing ConfigMap, schedulable requests), the ReplicaSet successfully creates pods and they reach
|
||||||
|
`Running` — no manual pod deletion needed, though you can delete a stuck pod to speed replacement.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get deployment redis-deployment # READY should match desired
|
||||||
|
kubectl get pods -l app=redis # all Running
|
||||||
|
kubectl describe deployment redis-deployment | sed -n '/Events/,$p'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — `redis-deployment` `READY N/N`, all pods `Running`, and no recurring error events.
|
||||||
|
|
||||||
|
> Paste the output of `kubectl get deployment redis-deployment -o yaml` if you want the exact
|
||||||
|
> one-line patch — the fix depends on which field was mistyped.
|
||||||
143
kubernetes/level 2/task-11.md
Normal file
143
kubernetes/level 2/task-11.md
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
One of the DevOps team members was trying to install a WordPress website on a LAMP stack, which is deployed on a Kubernetes cluster. It was working well, and we could see the installation page a few hours ago. However, something seems to have gone wrong with the stack after the website went down. Please look into the issue and fix it:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
FYI, the deployment name is lamp-wp and it is using a service named lamp-service. Apache is using the default HTTP port, and the NodePort is 30008. From the application logs, it has been identified that the application is facing some issues connecting to the database, in addition to other problems. Additionally, there are some environment variables associated with the pods, such as MYSQL_ROOT_PASSWORD, MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD, and MYSQL_HOST
|
||||||
|
|
||||||
|
Also, do not attempt to delete or modify any other existing components, such as deployment names, service names, types, labels, secrets and so on.
|
||||||
|
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Troubleshoot LAMP WordPress — `lamp-wp` / `lamp-service`
|
||||||
|
|
||||||
|
The WordPress site is down after a bad change. The hints point to **two+ problems**: a DB-connection
|
||||||
|
issue (env vars) and "other problems" (typically the service port). This diagnoses, then fixes
|
||||||
|
surgically — **without** touching deployment/service names, types, labels, or secrets, as required.
|
||||||
|
|
||||||
|
## Step 1 — Diagnose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pod + container state
|
||||||
|
kubectl get pods -l app=lamp-wp # adjust selector to match
|
||||||
|
kubectl describe pod -l app=lamp-wp
|
||||||
|
|
||||||
|
# App logs (the DB connection error shows here)
|
||||||
|
kubectl logs -l app=lamp-wp -c <httpd-or-php-container>
|
||||||
|
kubectl logs -l app=lamp-wp -c <mysql-container>
|
||||||
|
|
||||||
|
# Full specs — find the mismatches
|
||||||
|
kubectl get deployment lamp-wp -o yaml
|
||||||
|
kubectl get service lamp-service -o yaml
|
||||||
|
kubectl get secrets # note the secret + keys the env vars use
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for these specific mismatches:
|
||||||
|
|
||||||
|
1. **Service port** — Apache serves on **80** (default HTTP). The Service's `targetPort` must be
|
||||||
|
`80`; if the bad edit set it to something else (e.g. `8080`), the NodePort routes to a dead
|
||||||
|
port. `nodePort` stays `30008`.
|
||||||
|
2. **`MYSQL_HOST`** — in the WordPress/PHP container this must equal the **DB service name** (check
|
||||||
|
`kubectl get svc` for the MySQL service). If it points at the wrong host/IP, WordPress can't
|
||||||
|
reach the database — the logged connection error.
|
||||||
|
3. **DB credential env consistency** — the WordPress container's `MYSQL_ROOT_PASSWORD`,
|
||||||
|
`MYSQL_DATABASE`, `MYSQL_USER`, `MYSQL_PASSWORD` must match what the **MySQL** container was
|
||||||
|
initialized with (usually both pull from the same Secret keys). A key mismatch means WordPress
|
||||||
|
authenticates with wrong credentials.
|
||||||
|
|
||||||
|
## Step 2 — Fix (surgical; names/types/labels/secrets untouched)
|
||||||
|
|
||||||
|
### Service targetPort → 80
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl patch service lamp-service \
|
||||||
|
--type=json \
|
||||||
|
-p='[{"op":"replace","path":"/spec/ports/0/targetPort","value":80}]'
|
||||||
|
```
|
||||||
|
|
||||||
|
### `MYSQL_HOST` → the correct DB service name
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find the MySQL service name first
|
||||||
|
kubectl get svc
|
||||||
|
|
||||||
|
# Set MYSQL_HOST on the WordPress/PHP container (use its real container name)
|
||||||
|
kubectl set env deployment/lamp-wp \
|
||||||
|
--containers='<httpd-or-php-container>' \
|
||||||
|
MYSQL_HOST=<mysql-service-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Any wrong credential env / secret key reference
|
||||||
|
|
||||||
|
If an env var references a wrong Secret key, correct the **reference** (not the Secret) in place:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl edit deployment lamp-wp
|
||||||
|
# fix the mistyped valueFrom.secretKeyRef.key / .name to match `kubectl get secret <name> -o yaml`
|
||||||
|
```
|
||||||
|
|
||||||
|
Each change updates the pod template and triggers a fresh rollout:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl rollout status deployment/lamp-wp
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### Two independent failure planes
|
||||||
|
|
||||||
|
This stack breaks in two places that must both be right:
|
||||||
|
|
||||||
|
- **Network path** — `<node-ip>:30008` (nodePort) → Service `port` → **`targetPort` 80** → Apache.
|
||||||
|
If `targetPort` doesn't match Apache's listen port, the page is unreachable even when the pod is
|
||||||
|
perfectly healthy. That's the "other problem" beyond the DB.
|
||||||
|
- **App→DB path** — WordPress connects to MySQL over the cluster network using `MYSQL_HOST` plus the
|
||||||
|
credential env vars. `MYSQL_HOST` must resolve to the MySQL **Service** (stable DNS name), and the
|
||||||
|
credentials must match what MySQL was initialized with. A wrong host or mismatched credential is
|
||||||
|
the logged "can't connect to database" error.
|
||||||
|
|
||||||
|
Fixing one without the other leaves the site down, which is why the task hints at multiple issues.
|
||||||
|
|
||||||
|
### Why surgical edits, not re-apply
|
||||||
|
|
||||||
|
The task forbids changing deployment/service names, types, labels, and secrets. Re-applying a
|
||||||
|
hand-built manifest risks altering those by omission. `kubectl patch` (targetPort), `kubectl set
|
||||||
|
env` (one env var on one container), and `kubectl edit` (a single reference) each touch exactly the
|
||||||
|
broken field and leave every protected component intact. This is the safe way to honor the "don't
|
||||||
|
modify other components" rule.
|
||||||
|
|
||||||
|
### Why `MYSQL_HOST` is a Service name
|
||||||
|
|
||||||
|
Pod IPs are ephemeral; a Service gives MySQL a stable DNS name inside the cluster. WordPress must
|
||||||
|
target that name so it keeps resolving across pod restarts — hardcoding an IP or using a wrong name
|
||||||
|
breaks on the first reschedule, which is exactly the kind of thing a bad manual edit introduces.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pods running
|
||||||
|
kubectl get pods -l app=lamp-wp
|
||||||
|
|
||||||
|
# Service targetPort is 80, nodePort 30008
|
||||||
|
kubectl get service lamp-service \
|
||||||
|
-o jsonpath='{.spec.ports[0].targetPort}{" "}{.spec.ports[0].nodePort}{"\n"}'
|
||||||
|
|
||||||
|
# WordPress env has correct MYSQL_HOST
|
||||||
|
kubectl set env deployment/lamp-wp --list | grep MYSQL_HOST
|
||||||
|
|
||||||
|
# App reachable / DB connected (no more DB error in logs)
|
||||||
|
kubectl logs -l app=lamp-wp -c <httpd-or-php-container> | tail
|
||||||
|
curl -sI http://<node-ip>:30008 | head -n1
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — pods `Running`, service `targetPort 80` / `nodePort 30008`, `MYSQL_HOST` set to the
|
||||||
|
MySQL service name, no DB-connection errors in the logs, and the WordPress installation/login page
|
||||||
|
loading at `<node-ip>:30008`.
|
||||||
|
|
||||||
|
> Paste `kubectl get deployment lamp-wp -o yaml` and `kubectl get service lamp-service -o yaml`
|
||||||
|
> (plus `kubectl get secret <name> -o yaml`) and I'll give you the exact patches — the precise fix
|
||||||
|
> depends on which values were changed.
|
||||||
120
kubernetes/level 2/task-2.md
Normal file
120
kubernetes/level 2/task-2.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
Nautilus developers need access to the last 24 hours of logs so that they can trace issues and bugs. Therefore, we need to ship the access and error logs for the web server to a log-aggregation service. Following the separation of concerns principle, we implement the Sidecar pattern by deploying a second container that ships the error and access logs from nginx. Nginx does one thing, and it does it well - serving web pages. The second container also specializes in its task - shipping logs. Since containers are running on the same Pod, we can use a shared emptyDir volume to read and write logs.
|
||||||
|
|
||||||
|
|
||||||
|
Create a pod named webserver.
|
||||||
|
|
||||||
|
Create an emptyDir volume named shared-logs.
|
||||||
|
|
||||||
|
Create a regular container in the webserver pod from the nginx:latest image named nginx-container, and an init container from the ubuntu:latest image named sidecar-container.
|
||||||
|
|
||||||
|
Add the following command to the sidecar-container "sh","-c","while true; do cat /var/log/nginx/access.log /var/log/nginx/error.log; sleep 30; done"
|
||||||
|
|
||||||
|
Mount the shared-logs volume in both containers at /var/log/nginx. Ensure all containers are in a running state.
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Kubernetes Sidecar Logging Pod — `webserver`
|
||||||
|
|
||||||
|
An nginx container plus a log-shipping `sidecar-container`, sharing an `emptyDir` volume at
|
||||||
|
`/var/log/nginx`. The sidecar is defined as a **native sidecar** (an init container with
|
||||||
|
`restartPolicy: Always`) so it keeps running without blocking the pod. Applied inline via a
|
||||||
|
heredoc — no manifest file on disk.
|
||||||
|
|
||||||
|
## Apply (heredoc → kubectl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: webserver
|
||||||
|
spec:
|
||||||
|
volumes:
|
||||||
|
- name: shared-logs
|
||||||
|
emptyDir: {}
|
||||||
|
initContainers:
|
||||||
|
- name: sidecar-container
|
||||||
|
image: ubuntu:latest
|
||||||
|
restartPolicy: Always
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- "while true; do cat /var/log/nginx/access.log /var/log/nginx/error.log; sleep 30; done"
|
||||||
|
volumeMounts:
|
||||||
|
- name: shared-logs
|
||||||
|
mountPath: /var/log/nginx
|
||||||
|
containers:
|
||||||
|
- name: nginx-container
|
||||||
|
image: nginx:latest
|
||||||
|
volumeMounts:
|
||||||
|
- name: shared-logs
|
||||||
|
mountPath: /var/log/nginx
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The catch: "init container" that must keep running
|
||||||
|
|
||||||
|
The task calls `sidecar-container` an **init container**, gives it an infinite
|
||||||
|
`while true` loop, and requires **all containers to be running**. Those are contradictory for a
|
||||||
|
*classic* init container: a normal init container must run to **completion** before the main
|
||||||
|
container starts. An init container with `while true` never completes, so the pod would hang in
|
||||||
|
`Init:0/1` forever and nginx would never start.
|
||||||
|
|
||||||
|
### The resolution: a native sidecar
|
||||||
|
|
||||||
|
Kubernetes 1.28+ supports **native sidecar containers** — an entry under `initContainers` that
|
||||||
|
carries **`restartPolicy: Always`**. That one field changes the semantics:
|
||||||
|
|
||||||
|
- it's still declared under `initContainers` (satisfying "init container"),
|
||||||
|
- but with `restartPolicy: Always`, Kubernetes **starts it and moves on** without waiting for it
|
||||||
|
to complete, and **keeps it running** for the pod's lifetime alongside the main container.
|
||||||
|
|
||||||
|
So nginx starts normally, the sidecar runs continuously tailing the logs, and the pod reaches a
|
||||||
|
fully `Running` state — meeting both "init container" and "all containers running." This is
|
||||||
|
exactly the sidecar pattern the task describes, implemented the modern, native way.
|
||||||
|
|
||||||
|
### The shared volume
|
||||||
|
|
||||||
|
- **`shared-logs` (`emptyDir`)** is declared once and mounted at **`/var/log/nginx` in both
|
||||||
|
containers**. nginx writes `access.log` and `error.log` there (its default log directory);
|
||||||
|
the sidecar reads from the same path because it's the same underlying storage. This is the
|
||||||
|
"read and write logs via a shared emptyDir" mechanism the task calls for — separation of
|
||||||
|
concerns: nginx serves, the sidecar ships logs, they meet only at the shared volume.
|
||||||
|
|
||||||
|
### The sidecar command
|
||||||
|
|
||||||
|
`sh -c "while true; do cat .../access.log .../error.log; sleep 30; done"` continuously dumps the
|
||||||
|
two log files every 30 seconds (standing in for shipping them to an aggregator). The infinite
|
||||||
|
loop is what requires the native-sidecar treatment above — without `restartPolicy: Always` under
|
||||||
|
`initContainers`, this loop would block the pod.
|
||||||
|
|
||||||
|
> Note: nginx creates `access.log`/`error.log` on first request; until then the sidecar's `cat`
|
||||||
|
> may print "No such file or directory" to its own stdout, which is harmless — it keeps looping.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pod fully up: nginx running AND the native sidecar running
|
||||||
|
kubectl get pod webserver
|
||||||
|
|
||||||
|
# Confirm the sidecar is an init container with restartPolicy: Always
|
||||||
|
kubectl get pod webserver \
|
||||||
|
-o jsonpath='{.spec.initContainers[0].name}{" restartPolicy="}{.spec.initContainers[0].restartPolicy}{"\n"}'
|
||||||
|
|
||||||
|
# Both mounts point at /var/log/nginx
|
||||||
|
kubectl get pod webserver \
|
||||||
|
-o jsonpath='{range .spec.containers[*]}{.name}{" "}{.volumeMounts[0].mountPath}{"\n"}{end}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — `webserver` `Running` with the nginx container ready; the jsonpath printing
|
||||||
|
`sidecar-container restartPolicy=Always`; and the mount path `/var/log/nginx` on the containers.
|
||||||
|
|
||||||
|
> If the pod sits in `Init:0/1`, the sidecar is being treated as a **classic** init container —
|
||||||
|
> the `restartPolicy: Always` line is missing or the cluster predates native sidecars (needs
|
||||||
|
> Kubernetes 1.28+). Confirm the field made it into the spec with the second command above.
|
||||||
121
kubernetes/level 2/task-3.md
Normal file
121
kubernetes/level 2/task-3.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
Some of the Nautilus team developers are developing a static website and they want to deploy it on Kubernetes cluster. They want it to be highly available and scalable. Therefore, based on the requirements, the DevOps team has decided to create a deployment for it with multiple replicas. Below you can find more details about it:
|
||||||
|
|
||||||
|
|
||||||
|
Create a deployment using nginx image with latest tag only and remember to mention the tag i.e nginx:latest. Name it as nginx-deployment. The container should be named as nginx-container, also make sure replica counts are 3.
|
||||||
|
|
||||||
|
Create a NodePort type service named nginx-service. The nodePort should be 30011.
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Kubernetes Deployment + NodePort Service — `nginx-deployment` / `nginx-service`
|
||||||
|
|
||||||
|
A 3-replica nginx Deployment exposed by a NodePort Service on port `30011`. Both objects in one
|
||||||
|
multi-document heredoc — no manifest file on disk.
|
||||||
|
|
||||||
|
## Apply (heredoc → kubectl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: nginx-deployment
|
||||||
|
labels:
|
||||||
|
app: nginx
|
||||||
|
spec:
|
||||||
|
replicas: 3
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: nginx
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: nginx
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: nginx-container
|
||||||
|
image: nginx:latest
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: nginx-service
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
selector:
|
||||||
|
app: nginx
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 80
|
||||||
|
nodePort: 30011
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The heredoc apply pattern
|
||||||
|
|
||||||
|
- **`kubectl apply -f -`** reads from **stdin**; `---` separates the two documents, applied in
|
||||||
|
order.
|
||||||
|
- **`<<'EOF'` (delimiter quoted)** keeps the manifest literal — the right default for k8s YAML.
|
||||||
|
|
||||||
|
### The Deployment
|
||||||
|
|
||||||
|
- **`apps/v1` / `kind: Deployment`** — manages a ReplicaSet, which keeps the desired number of
|
||||||
|
pods running and self-heals; the basis for the "highly available and scalable" requirement.
|
||||||
|
- **`metadata.name: nginx-deployment`** and container **`name: nginx-container`** — exactly as
|
||||||
|
required.
|
||||||
|
- **`replicas: 3`** — three identical pods spread across the cluster for availability.
|
||||||
|
- **`image: nginx:latest`** — tag stated explicitly, as required.
|
||||||
|
- **`selector.matchLabels: app: nginx`** must equal **`template.metadata.labels: app: nginx`** —
|
||||||
|
a Deployment finds the pods it owns by this label match; a mismatch makes the API reject the
|
||||||
|
manifest.
|
||||||
|
|
||||||
|
### The Service ties to pods by label, not to the Deployment
|
||||||
|
|
||||||
|
- **`type: NodePort`** opens a port on every node that forwards into the cluster.
|
||||||
|
- **`selector: app: nginx`** — this is what connects the Service to the Deployment's pods. The
|
||||||
|
Service targets any pod labeled `app: nginx` — which is exactly what the Deployment stamps on
|
||||||
|
its three pods — and load-balances across them. The Service has no direct reference to the
|
||||||
|
Deployment; the shared `app: nginx` label is the entire link. A mismatched selector would
|
||||||
|
leave the Service with **zero endpoints**.
|
||||||
|
- **Port fields:**
|
||||||
|
- `port: 80` — the Service's own ClusterIP port.
|
||||||
|
- `targetPort: 80` — the container port traffic is forwarded to (nginx serves on 80).
|
||||||
|
- `nodePort: 30011` — the port opened on each node for external access. `30011` is inside the
|
||||||
|
valid range (`30000–32767`), so the API accepts it.
|
||||||
|
|
||||||
|
Traffic path: `<node-ip>:30011` → Service `:80` → one of the three pods `:80`.
|
||||||
|
|
||||||
|
### Why the label must be consistent in three places
|
||||||
|
|
||||||
|
`app: nginx` appears in the Deployment selector, the pod template labels, and the Service
|
||||||
|
selector. The first two tie the Deployment to its pods; the third ties the Service to those same
|
||||||
|
pods. Keeping the label identical across all three is what makes the whole chain — Deployment →
|
||||||
|
pods → Service — connect.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Deployment with 3 ready replicas
|
||||||
|
kubectl get deployment nginx-deployment
|
||||||
|
|
||||||
|
# Service is NodePort on 30011
|
||||||
|
kubectl get service nginx-service
|
||||||
|
|
||||||
|
# Endpoints populated = selector matched the pods (key check)
|
||||||
|
kubectl get endpoints nginx-service
|
||||||
|
|
||||||
|
# Reachable via the node port
|
||||||
|
curl -s http://<node-ip>:30011 | head -n 5
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — deployment `READY 3/3`, `nginx-service` of type `NodePort` showing `80:30011/TCP`,
|
||||||
|
and `kubectl get endpoints nginx-service` listing three pod IPs. Empty endpoints ⇒ selector/label
|
||||||
|
mismatch (check `kubectl get pods --show-labels`).
|
||||||
123
kubernetes/level 2/task-4.md
Normal file
123
kubernetes/level 2/task-4.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
The Nautilus DevOps team is working on to setup some pre-requisites for an application that will send the greetings to different users. There is a sample deployment, that needs to be tested. Below is a scenario which needs to be configured on Kubernetes cluster. Please find below more details about it.
|
||||||
|
|
||||||
|
|
||||||
|
Create a pod named print-envars-greeting.
|
||||||
|
|
||||||
|
Configure spec as, the container name should be print-env-container and use bash image.
|
||||||
|
|
||||||
|
Create three environment variables:
|
||||||
|
|
||||||
|
a. GREETING and its value should be Welcome to
|
||||||
|
|
||||||
|
b. COMPANY and its value should be DevOps
|
||||||
|
|
||||||
|
c. GROUP and its value should be Industries
|
||||||
|
|
||||||
|
Use command ["/bin/sh", "-c", 'echo "$(GREETING) $(COMPANY) $(GROUP)"'] (please use this exact command), also set its restartPolicy policy to Never to avoid crash loop back.
|
||||||
|
|
||||||
|
You can check the output using kubectl logs -f print-envars-greeting command.
|
||||||
|
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Kubernetes Pod with Env Vars — `print-envars-greeting`
|
||||||
|
|
||||||
|
A one-shot pod that echoes three environment variables using Kubernetes' `$(VAR)` substitution.
|
||||||
|
Applied inline via a heredoc — no manifest file on disk.
|
||||||
|
|
||||||
|
## Apply (heredoc → kubectl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: print-envars-greeting
|
||||||
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
|
containers:
|
||||||
|
- name: print-env-container
|
||||||
|
image: bash
|
||||||
|
command: ["/bin/sh", "-c", 'echo "$(GREETING) $(COMPANY) $(GROUP)"']
|
||||||
|
env:
|
||||||
|
- name: GREETING
|
||||||
|
value: "Welcome to"
|
||||||
|
- name: COMPANY
|
||||||
|
value: "DevOps"
|
||||||
|
- name: GROUP
|
||||||
|
value: "Industries"
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## Check the output
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl logs -f print-envars-greeting
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `Welcome to DevOps Industries`
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The heredoc apply pattern — quoting matters a lot here
|
||||||
|
|
||||||
|
- **`kubectl apply -f -`** reads from **stdin**; nothing written to disk.
|
||||||
|
- **`<<'EOF'` (delimiter quoted)** is critical for this task. The command contains `$(GREETING)`,
|
||||||
|
`$(COMPANY)`, `$(GROUP)`. If the heredoc delimiter were **unquoted**, the jump-host's shell
|
||||||
|
would interpret `$(...)` as **command substitution** and try to run `GREETING` as a command —
|
||||||
|
mangling the manifest before kubectl ever sees it. Quoting `EOF` passes the `$(...)` through
|
||||||
|
literally so Kubernetes (not the shell) handles the expansion.
|
||||||
|
|
||||||
|
### `$(VAR)` is Kubernetes substitution, not shell substitution
|
||||||
|
|
||||||
|
The exact command uses `$(GREETING)`, not `${GREETING}` or `$GREETING`. In a container's
|
||||||
|
`command`/`args`, **Kubernetes itself** expands `$(VAR)` references using the container's declared
|
||||||
|
`env` **before** the process starts. So Kubernetes rewrites the command to
|
||||||
|
`echo "Welcome to DevOps Industries"`, and `/bin/sh` then just echoes that literal string.
|
||||||
|
|
||||||
|
This is a subtle but important distinction:
|
||||||
|
- **`$(VAR)`** — expanded by **Kubernetes** from the `env` list at container start.
|
||||||
|
- **`${VAR}` / `$VAR`** — expanded by the **shell** at runtime.
|
||||||
|
|
||||||
|
Both would produce the same output here (since `sh` also has the env vars), but the task pins the
|
||||||
|
`$(VAR)` form, and it's Kubernetes doing the substitution. A gotcha to know: `$(VAR)` only
|
||||||
|
resolves if a matching `env` entry exists; an undefined `$(FOO)` is left **literal** rather than
|
||||||
|
erroring.
|
||||||
|
|
||||||
|
### The env vars
|
||||||
|
|
||||||
|
Three `env` entries — `GREETING="Welcome to"`, `COMPANY="DevOps"`, `GROUP="Industries"` — supply
|
||||||
|
the values Kubernetes substitutes into the command. Values are quoted because they contain spaces
|
||||||
|
(`Welcome to`).
|
||||||
|
|
||||||
|
### `restartPolicy: Never`
|
||||||
|
|
||||||
|
The command echoes once and exits `0` — a completed run, not a long-running process. With the
|
||||||
|
default `restartPolicy: Always`, Kubernetes would see the container exit and keep restarting it,
|
||||||
|
driving `CrashLoopBackOff` (even though it "succeeded"). `Never` tells Kubernetes not to restart
|
||||||
|
it, so the pod ends in `Completed` state cleanly. This is why the task specifies it.
|
||||||
|
|
||||||
|
### The `bash` image
|
||||||
|
|
||||||
|
`image: bash` pulls the official BusyBox-based bash image, which provides `/bin/sh` to run the
|
||||||
|
command. It runs the echo and exits — exactly the one-shot behavior wanted.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pod ran to completion
|
||||||
|
kubectl get pod print-envars-greeting # STATUS: Completed
|
||||||
|
|
||||||
|
# The greeting output
|
||||||
|
kubectl logs print-envars-greeting
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — `print-envars-greeting` in `Completed` status, and the logs printing
|
||||||
|
`Welcome to DevOps Industries`.
|
||||||
|
|
||||||
|
> `Completed` (not `Running`) is correct here — the pod's job was to print once and exit. If it
|
||||||
|
> shows `CrashLoopBackOff`, `restartPolicy: Never` didn't take.
|
||||||
163
kubernetes/level 2/task-5.md
Normal file
163
kubernetes/level 2/task-5.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
There is a production deployment planned for next week. The Nautilus DevOps team wants to test the deployment update and rollback on Dev environment first so that they can identify the risks in advance. Below you can find more details about the plan they want to execute.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Create a namespace xfusion. Create a deployment called httpd-deploy under this new namespace, It should have one container called httpd, use httpd:2.4.27 image and 3 replicas. The deployment should use RollingUpdate strategy with maxSurge=1, and maxUnavailable=2. Also create a NodePort type service named httpd-service and expose the deployment on nodePort: 30008.
|
||||||
|
|
||||||
|
|
||||||
|
Now upgrade the deployment to version httpd:2.4.43 using a rolling update.
|
||||||
|
|
||||||
|
|
||||||
|
Finally, once all pods are updated undo the recent update and roll back to the previous/original version.
|
||||||
|
|
||||||
|
|
||||||
|
Note:
|
||||||
|
|
||||||
|
a. The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
|
||||||
|
b. Please make sure you only use the specified image(s) for this deployment and as per the sequence mentioned in the task description. If you mistakenly use a wrong image and fix it later, that will also distort the revision history which can eventually fail this task.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Deployment Update + Rollback — `httpd-deploy` in `devops`
|
||||||
|
|
||||||
|
Create the deployment at `httpd:2.4.27`, roll it forward to `httpd:2.4.43`, then roll it back —
|
||||||
|
keeping revision history clean throughout.
|
||||||
|
|
||||||
|
> **Sequencing matters:** apply each image one at a time and let each rollout finish before the
|
||||||
|
> next. Do **not** put `2.4.43` in the initial manifest, and don't "fix" a wrong image mid-stream
|
||||||
|
> — either distorts the revision history.
|
||||||
|
|
||||||
|
## Step 1 — Namespace + deployment (`2.4.27`) + service
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: devops
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: httpd-deploy
|
||||||
|
namespace: devops
|
||||||
|
labels:
|
||||||
|
app: httpd
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
strategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
rollingUpdate:
|
||||||
|
maxSurge: 1
|
||||||
|
maxUnavailable: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: httpd
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: httpd
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: httpd
|
||||||
|
image: httpd:2.4.27
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: httpd-service
|
||||||
|
namespace: devops
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
selector:
|
||||||
|
app: httpd
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 80
|
||||||
|
nodePort: 30008
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Let revision 1 fully settle BEFORE any update
|
||||||
|
kubectl rollout status deployment/httpd-deploy -n devops
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2 — Rolling update to `httpd:2.4.43`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl set image deployment/httpd-deploy httpd=httpd:2.4.43 -n devops
|
||||||
|
|
||||||
|
# Wait for the update to fully complete (revision 2)
|
||||||
|
kubectl rollout status deployment/httpd-deploy -n devops
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3 — Roll back to the original version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl rollout undo deployment/httpd-deploy -n devops
|
||||||
|
|
||||||
|
# Wait for the rollback to complete (revision 3, template of revision 1)
|
||||||
|
kubectl rollout status deployment/httpd-deploy -n devops
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### Sequencing keeps revision history clean
|
||||||
|
|
||||||
|
Each `kubectl` action that changes the pod template creates a new **revision**, each backed by a
|
||||||
|
ReplicaSet:
|
||||||
|
|
||||||
|
- **Revision 1** — the initial `2.4.27` deployment.
|
||||||
|
- **Revision 2** — the `set image` update to `2.4.43`.
|
||||||
|
- **Revision 3** — the `rollout undo`, which reuses revision 1's template (`2.4.27`).
|
||||||
|
|
||||||
|
`kubectl rollout status` between each step forces each rollout to **finish** before the next
|
||||||
|
begins, so the revisions are distinct and ordered. Firing the update before the create settles —
|
||||||
|
or applying a wrong image and correcting it — would inject extra revisions.
|
||||||
|
|
||||||
|
### The initial manifest
|
||||||
|
|
||||||
|
- **Namespace `devops`** created first so the deployment and service land in it.
|
||||||
|
- **Deployment** — `httpd-deploy`, container **`httpd`**, image **`httpd:2.4.27`**, `replicas: 2`.
|
||||||
|
- **`strategy.rollingUpdate`** — `maxSurge: 1` (at most 1 pod above the desired 2 during a
|
||||||
|
rollout) and `maxUnavailable: 2` (up to 2 pods down at once). With only 2 replicas and
|
||||||
|
`maxUnavailable: 2`, both pods can be replaced at once, so the rollout is fast.
|
||||||
|
- **`selector.matchLabels: app: httpd`** equals the template labels, and the **Service**'s
|
||||||
|
`selector: app: httpd` targets those same pods. `nodePort: 30008` is in the valid range
|
||||||
|
(`30000–32767`).
|
||||||
|
|
||||||
|
### `set image` for the update, `rollout undo` for the rollback
|
||||||
|
|
||||||
|
- **`set image`** patches just the container image on the live deployment — the idiomatic way to
|
||||||
|
trigger a rolling update. Container name `httpd` is the left side of `httpd=httpd:2.4.43`.
|
||||||
|
- **`rollout undo`** with no `--to-revision` reverts to the **immediately previous** revision
|
||||||
|
(revision 1's `2.4.27` template) — exactly "the previous/original version" — and records it as a
|
||||||
|
new forward revision (3), preserving the audit trail.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Current image after rollback should be the ORIGINAL 2.4.27
|
||||||
|
kubectl get deployment httpd-deploy -n devops \
|
||||||
|
-o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
|
||||||
|
|
||||||
|
# Revision history: expect revisions 1, 2, 3 in order
|
||||||
|
kubectl rollout history deployment/httpd-deploy -n devops
|
||||||
|
|
||||||
|
# All pods ready, service exposed on 30008
|
||||||
|
kubectl get deployment httpd-deploy -n devops
|
||||||
|
kubectl get service httpd-service -n devops
|
||||||
|
kubectl get endpoints httpd-service -n devops
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — after Step 3 the image is back to `httpd:2.4.27`, rollout history shows three
|
||||||
|
revisions, deployment `READY 2/2`, and `httpd-service` NodePort `80:30008/TCP` with two
|
||||||
|
endpoints.
|
||||||
|
|
||||||
|
> Everything is in `devops` — keep `-n devops` on every command. If endpoints is empty, the
|
||||||
|
> service selector doesn't match the pod labels.
|
||||||
140
kubernetes/level 2/task-6.md
Normal file
140
kubernetes/level 2/task-6.md
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
The Nautilus DevOps team is planning to set up a Jenkins CI server to create/manage some deployment pipelines for some of the projects. They want to set up the Jenkins server on Kubernetes cluster. Below you can find more details about the task:
|
||||||
|
|
||||||
|
|
||||||
|
1) Create a namespace jenkins
|
||||||
|
|
||||||
|
2) Create a Service for jenkins deployment. Service name should be jenkins-service under jenkins namespace, type should be NodePort, nodePort should be 30008
|
||||||
|
|
||||||
|
3) Create a Jenkins Deployment under jenkins namespace, It should be name as jenkins-deployment , labels app should be jenkins , container name should be jenkins-container , use jenkins/jenkins image , containerPort should be 8080 and replicas count should be 1. It should also have the environment variable JAVA_OPTS with the value -Djenkins.install.runSetupWizard=false to skip the initial setup wizard.
|
||||||
|
|
||||||
|
Make sure to wait for the pods to be in running state and make sure you are able to access the Jenkins UI screen in the browser before hitting the Check button.
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Jenkins on Kubernetes — `jenkins-deployment` + `jenkins-service` in `jenkins`
|
||||||
|
|
||||||
|
A Jenkins Deployment exposed via a NodePort Service, with the setup wizard disabled. All objects
|
||||||
|
in one multi-document heredoc — no manifest file on disk.
|
||||||
|
|
||||||
|
## Apply (heredoc → kubectl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: jenkins
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: jenkins-deployment
|
||||||
|
namespace: jenkins
|
||||||
|
labels:
|
||||||
|
app: jenkins
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: jenkins
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: jenkins
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: jenkins-container
|
||||||
|
image: jenkins/jenkins
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
env:
|
||||||
|
- name: JAVA_OPTS
|
||||||
|
value: "-Djenkins.install.runSetupWizard=false"
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: jenkins-service
|
||||||
|
namespace: jenkins
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
selector:
|
||||||
|
app: jenkins
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
||||||
|
targetPort: 8080
|
||||||
|
nodePort: 30008
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Wait for Jenkins to come up (it takes a bit to initialize)
|
||||||
|
kubectl rollout status deployment/jenkins-deployment -n jenkins
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The heredoc apply pattern
|
||||||
|
|
||||||
|
- **`kubectl apply -f -`** reads from **stdin**; `---` separates the three documents, applied in
|
||||||
|
order so the **Namespace** exists before the Deployment and Service land in it.
|
||||||
|
- **`<<'EOF'` (delimiter quoted)** keeps the manifest literal — important here so `JAVA_OPTS`'s
|
||||||
|
value (with its `-D...` flag) isn't mangled by the jump-host shell.
|
||||||
|
|
||||||
|
### The Deployment
|
||||||
|
|
||||||
|
- **`namespace: jenkins`, `name: jenkins-deployment`, `labels.app: jenkins`** — exactly as
|
||||||
|
required.
|
||||||
|
- **container `name: jenkins-container`, `image: jenkins/jenkins`** — the official Jenkins image.
|
||||||
|
- **`containerPort: 8080`** — Jenkins' web UI listens on 8080 inside the container.
|
||||||
|
- **`replicas: 1`** — a single Jenkins instance.
|
||||||
|
- **`env: JAVA_OPTS = -Djenkins.install.runSetupWizard=false`** — passed to Jenkins' JVM. This
|
||||||
|
system property **skips the initial setup wizard** (the "unlock Jenkins / install plugins"
|
||||||
|
screens), so the UI is immediately usable — which is what lets the Check succeed without manual
|
||||||
|
unlock steps.
|
||||||
|
- **`selector.matchLabels: app: jenkins`** equals the template labels — the Deployment↔pod link.
|
||||||
|
|
||||||
|
### The Service — port 8080 is the key detail
|
||||||
|
|
||||||
|
- **`type: NodePort`** exposes Jenkins outside the cluster.
|
||||||
|
- **`selector: app: jenkins`** targets the deployment's pod by its label.
|
||||||
|
- **Port mapping:**
|
||||||
|
- `port: 8080` — the Service's ClusterIP port.
|
||||||
|
- **`targetPort: 8080`** — the container port. This **must** be 8080 because that's where
|
||||||
|
Jenkins actually listens; pointing it elsewhere would give you a Service with endpoints but no
|
||||||
|
response.
|
||||||
|
- `nodePort: 30008` — the external port on each node (in the valid `30000–32767` range).
|
||||||
|
|
||||||
|
Path: `<node-ip>:30008` → Service `:8080` → Jenkins pod `:8080`.
|
||||||
|
|
||||||
|
### Why waiting matters
|
||||||
|
|
||||||
|
Jenkins takes noticeably longer than a typical app to become ready — the JVM boots, unpacks
|
||||||
|
plugins, and initializes before the UI responds. `kubectl rollout status` blocks until the pod is
|
||||||
|
Ready, but the **UI** may need a few extra seconds after that. Confirm the login/dashboard
|
||||||
|
actually loads in the browser before hitting Check, as the task instructs.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pod running
|
||||||
|
kubectl get pods -n jenkins
|
||||||
|
|
||||||
|
# Service exposed on 30008 with an endpoint
|
||||||
|
kubectl get service jenkins-service -n jenkins
|
||||||
|
kubectl get endpoints jenkins-service -n jenkins
|
||||||
|
|
||||||
|
# Jenkins responding inside the cluster (should return HTTP 200/403, not connection refused)
|
||||||
|
kubectl exec -n jenkins deploy/jenkins-deployment -- \
|
||||||
|
sh -c 'curl -sI http://localhost:8080 | head -n1' 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — the Jenkins pod `Running` (`READY 1/1`), `jenkins-service` NodePort `8080:30008/TCP`
|
||||||
|
with one endpoint, and the Jenkins dashboard loading at `<node-ip>:30008` in the browser (no setup
|
||||||
|
wizard, thanks to `JAVA_OPTS`).
|
||||||
|
|
||||||
|
> If the browser shows "unlock Jenkins," the `JAVA_OPTS` env var didn't take — confirm with
|
||||||
|
> `kubectl get deploy jenkins-deployment -n jenkins -o jsonpath='{.spec.template.spec.containers[0].env}'`.
|
||||||
|
> Give the pod a minute; Jenkins' first boot is slow.
|
||||||
169
kubernetes/level 2/task-7.md
Normal file
169
kubernetes/level 2/task-7.md
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
The Nautilus DevOps teams is planning to set up a Grafana tool to collect and analyze analytics from some applications. They are planning to deploy it on Kubernetes cluster. Below you can find more details.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
1.) Create a deployment named grafana-deployment-xfusion using any grafana image for Grafana app. Set other parameters as per your choice.
|
||||||
|
|
||||||
|
|
||||||
|
2.) Create NodePort type service with nodePort 32000 to expose the app.
|
||||||
|
|
||||||
|
|
||||||
|
You do not need to make any configuration changes inside the Grafana app once deployed; just make sure you can access the Grafana login page.
|
||||||
|
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Grafana on Kubernetes — `grafana-deployment-xfusion` + NodePort Service
|
||||||
|
|
||||||
|
A Grafana Deployment exposed via a NodePort Service on `32000`. Both objects in one
|
||||||
|
multi-document heredoc — no manifest file on disk.
|
||||||
|
|
||||||
|
## Apply (heredoc → kubectl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: grafana-deployment-datacenter
|
||||||
|
labels:
|
||||||
|
app: grafana
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: grafana
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: grafana
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: grafana-container
|
||||||
|
image: grafana/grafana:latest
|
||||||
|
ports:
|
||||||
|
- containerPort: 32000
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: grafana-service
|
||||||
|
labels:
|
||||||
|
app: grafana
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
selector:
|
||||||
|
app: grafana
|
||||||
|
ports:
|
||||||
|
- port: 32000
|
||||||
|
targetPort: 3000
|
||||||
|
nodePort: 32000
|
||||||
|
EOF
|
||||||
|
|
||||||
|
kubectl rollout status deployment/kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: grafana-deployment-datacenter
|
||||||
|
labels:
|
||||||
|
app: grafana
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: grafana
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: grafana
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: grafana-container
|
||||||
|
image: grafana/grafana:latest
|
||||||
|
ports:
|
||||||
|
- containerPort: 32000
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: grafana-service
|
||||||
|
labels:
|
||||||
|
app: grafana
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
selector:
|
||||||
|
app: grafana
|
||||||
|
ports:
|
||||||
|
- port: 32000
|
||||||
|
targetPort: 3000
|
||||||
|
nodePort: 32000
|
||||||
|
EOF
|
||||||
|
|
||||||
|
kubectl rollout status deployment grafana-deployment-datacenter
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The heredoc apply pattern
|
||||||
|
|
||||||
|
- **`kubectl apply -f -`** reads from **stdin**; `---` separates the two documents.
|
||||||
|
- **`<<'EOF'` (delimiter quoted)** keeps the manifest literal — the right default for k8s YAML.
|
||||||
|
|
||||||
|
### The Deployment
|
||||||
|
|
||||||
|
- **`name: grafana-deployment-xfusion`, `labels.app: grafana`** — the required name plus a label
|
||||||
|
used to tie the Service to the pods.
|
||||||
|
- **`image: grafana/grafana:latest`** — the official Grafana image. The task allows any Grafana
|
||||||
|
image; the official `grafana/grafana` is the standard pick.
|
||||||
|
- **`containerPort: 3000`** — Grafana's web UI listens on **3000** inside the container. This is
|
||||||
|
the detail that everything else keys off.
|
||||||
|
- **`replicas: 1`** and container name `grafana-container` — free-choice parameters, kept minimal.
|
||||||
|
- **`selector.matchLabels: app: grafana`** equals the template labels — the Deployment↔pod link.
|
||||||
|
|
||||||
|
### The Service — targetPort 3000 is the crux
|
||||||
|
|
||||||
|
- **`type: NodePort`** exposes Grafana outside the cluster.
|
||||||
|
- **`selector: app: grafana`** targets the deployment's pod by its label.
|
||||||
|
- **Port mapping:**
|
||||||
|
- `port: 3000` — the Service's ClusterIP port.
|
||||||
|
- **`targetPort: 3000`** — the container port. This **must** be 3000 because that's where
|
||||||
|
Grafana listens; pointing it elsewhere yields a Service with an endpoint but no response, and
|
||||||
|
the login page wouldn't load.
|
||||||
|
- `nodePort: 32000` — the external port on each node, exactly as required (in the valid
|
||||||
|
`30000–32767` range).
|
||||||
|
|
||||||
|
Path: `<node-ip>:32000` → Service `:3000` → Grafana pod `:3000`.
|
||||||
|
|
||||||
|
### Why no in-app config is needed
|
||||||
|
|
||||||
|
The task only requires reaching the **login page** — Grafana serves its login UI out of the box
|
||||||
|
on port 3000 with no configuration. So the whole job is: run the container and route a NodePort to
|
||||||
|
3000. Default admin credentials are `admin` / `admin`, but you don't need to log in — just confirm
|
||||||
|
the login screen renders.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pod running
|
||||||
|
kubectl get deployment grafana-deployment-xfusion
|
||||||
|
kubectl get pods -l app=grafana
|
||||||
|
|
||||||
|
# Service exposed on 32000 with an endpoint
|
||||||
|
kubectl get service grafana-service
|
||||||
|
kubectl get endpoints grafana-service
|
||||||
|
|
||||||
|
# Grafana responding inside the cluster
|
||||||
|
kubectl exec deploy/grafana-deployment-xfusion -- \
|
||||||
|
sh -c 'wget -qO- http://localhost:3000/login >/dev/null && echo ok' 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — deployment `READY 1/1`, `grafana-service` NodePort `3000:32000/TCP` with one endpoint,
|
||||||
|
and the Grafana **login page** loading at `<node-ip>:32000` in the browser.
|
||||||
|
|
||||||
|
> If the page doesn't load, check `targetPort` is `3000` and the pod is Running. Grafana starts
|
||||||
|
> reasonably fast, but give it a few seconds after `rollout status` reports ready.
|
||||||
142
kubernetes/level 2/task-8.md
Normal file
142
kubernetes/level 2/task-8.md
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
A new java-based application is ready to be deployed on a Kubernetes cluster. The development team had a meeting with the DevOps team to share the requirements and application scope. The team is ready to setup an application stack for it under their existing cluster. Below you can find the details for this:
|
||||||
|
|
||||||
|
|
||||||
|
Create a namespace named tomcat-namespace-datacenter.
|
||||||
|
|
||||||
|
Create a deployment for tomcat app which should be named as tomcat-deployment-datacenter under the same namespace you created. Replica count should be 1, the container should be named as tomcat-container-datacenter, its image should be kodekloud/centos-ssh-enabled:tomcat and its container port should be 8080.
|
||||||
|
|
||||||
|
Create a service for tomcat app which should be named as tomcat-service-datacenter under the same namespace you created. Service type should be NodePort and nodePort should be 32227.
|
||||||
|
|
||||||
|
|
||||||
|
Before clicking on Check button please make sure the application is up and running.
|
||||||
|
|
||||||
|
|
||||||
|
You can use any labels as per your choice.
|
||||||
|
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Tomcat on Kubernetes — namespace + deployment + service (`datacenter`)
|
||||||
|
|
||||||
|
A Tomcat Deployment and NodePort Service inside a dedicated namespace. All objects in one
|
||||||
|
multi-document heredoc — no manifest file on disk.
|
||||||
|
|
||||||
|
## Apply (heredoc → kubectl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: tomcat-namespace-datacenter
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: tomcat-deployment-datacenter
|
||||||
|
namespace: tomcat-namespace-datacenter
|
||||||
|
labels:
|
||||||
|
app: tomcat
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: tomcat
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: tomcat
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: tomcat-container-datacenter
|
||||||
|
image: kodekloud/centos-ssh-enabled:tomcat
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: tomcat-service-datacenter
|
||||||
|
namespace: tomcat-namespace-datacenter
|
||||||
|
labels:
|
||||||
|
app: tomcat
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
selector:
|
||||||
|
app: tomcat
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
||||||
|
targetPort: 8080
|
||||||
|
nodePort: 32227
|
||||||
|
EOF
|
||||||
|
|
||||||
|
kubectl rollout status deployment/tomcat-deployment-datacenter -n tomcat-namespace-datacenter
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The heredoc apply pattern
|
||||||
|
|
||||||
|
- **`kubectl apply -f -`** reads from **stdin**; `---` separates the three documents, applied in
|
||||||
|
order so the **Namespace** exists before the Deployment and Service land in it.
|
||||||
|
- **`<<'EOF'` (delimiter quoted)** keeps the manifest literal — the right default for k8s YAML.
|
||||||
|
|
||||||
|
### Everything scoped to one namespace
|
||||||
|
|
||||||
|
All three objects carry `namespace: tomcat-namespace-datacenter` (the Namespace defines it, the
|
||||||
|
Deployment and Service reference it). Creating the namespace first in the same stream avoids a
|
||||||
|
`namespaces "..." not found` error. Because the Service and Deployment are in the same namespace,
|
||||||
|
the Service can select the deployment's pods by label directly.
|
||||||
|
|
||||||
|
### The Deployment
|
||||||
|
|
||||||
|
- **`name: tomcat-deployment-datacenter`**, container **`name: tomcat-container-datacenter`**,
|
||||||
|
**`image: kodekloud/centos-ssh-enabled:tomcat`**, **`replicas: 1`** — all exactly as required.
|
||||||
|
- **`containerPort: 8080`** — Tomcat serves on 8080 inside the container.
|
||||||
|
- **`app: tomcat`** label (free choice) on the template — ties the Deployment to its pods and the
|
||||||
|
Service to those pods.
|
||||||
|
- **`selector.matchLabels: app: tomcat`** equals the template labels — the Deployment↔pod link.
|
||||||
|
|
||||||
|
### The Service — targetPort 8080
|
||||||
|
|
||||||
|
- **`type: NodePort`** exposes Tomcat outside the cluster.
|
||||||
|
- **`selector: app: tomcat`** targets the deployment's pod by label.
|
||||||
|
- **Port mapping:**
|
||||||
|
- `port: 8080` — the Service's ClusterIP port.
|
||||||
|
- **`targetPort: 8080`** — the container port; must be 8080 to reach Tomcat.
|
||||||
|
- `nodePort: 32227` — the external port on each node, exactly as required (valid
|
||||||
|
`30000–32767` range).
|
||||||
|
|
||||||
|
Path: `<node-ip>:32227` → Service `:8080` → Tomcat pod `:8080`.
|
||||||
|
|
||||||
|
### "Up and running" before Check
|
||||||
|
|
||||||
|
`kubectl rollout status` blocks until the pod is Ready. This image bundles Tomcat on a CentOS/SSH
|
||||||
|
base, so give it a moment to start the Tomcat service after the pod reports Ready, then confirm the
|
||||||
|
app responds at `<node-ip>:32227`.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pod running in the namespace
|
||||||
|
kubectl get deployment tomcat-deployment-datacenter -n tomcat-namespace-datacenter
|
||||||
|
kubectl get pods -n tomcat-namespace-datacenter -l app=tomcat
|
||||||
|
|
||||||
|
# Service on 32227 with an endpoint
|
||||||
|
kubectl get service tomcat-service-datacenter -n tomcat-namespace-datacenter
|
||||||
|
kubectl get endpoints tomcat-service-datacenter -n tomcat-namespace-datacenter
|
||||||
|
|
||||||
|
# Tomcat responding
|
||||||
|
kubectl exec -n tomcat-namespace-datacenter deploy/tomcat-deployment-datacenter -- \
|
||||||
|
sh -c 'curl -sI http://localhost:8080 | head -n1' 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — deployment `READY 1/1`, `tomcat-service-datacenter` NodePort `8080:32227/TCP` with one
|
||||||
|
endpoint, and Tomcat reachable at `<node-ip>:32227`.
|
||||||
|
|
||||||
|
> Everything is in `tomcat-namespace-datacenter` — keep `-n tomcat-namespace-datacenter` on every
|
||||||
|
> command. If endpoints is empty, the service selector doesn't match the pod labels.
|
||||||
130
kubernetes/level 2/task-9.md
Normal file
130
kubernetes/level 2/task-9.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
The Nautilus development team has completed development of one of the node applications, which they are planning to deploy on a Kubernetes cluster. They recently had a meeting with the DevOps team to share their requirements. Based on that, the DevOps team has listed out the exact requirements to deploy the app. Find below more details:
|
||||||
|
|
||||||
|
|
||||||
|
Create a deployment using kodekloud/centos-ssh-enabled:node image, replica count must be 2.
|
||||||
|
|
||||||
|
Create a service to expose this app, the service type must be NodePort, targetPort must be 8080 and nodePort should be 30012.
|
||||||
|
|
||||||
|
Make sure all the pods are in Running state after the deployment.
|
||||||
|
|
||||||
|
You can check the application by clicking on NodeApp button on top bar.
|
||||||
|
|
||||||
|
|
||||||
|
You can use any labels as per your choice.
|
||||||
|
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Node App on Kubernetes — Deployment + NodePort Service
|
||||||
|
|
||||||
|
A 2-replica Node app Deployment exposed via a NodePort Service. Names/labels are free choice; the
|
||||||
|
required values are the image, replica count, and port mapping. One multi-document heredoc — no
|
||||||
|
manifest file on disk.
|
||||||
|
|
||||||
|
## Apply (heredoc → kubectl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: node-deployment
|
||||||
|
labels:
|
||||||
|
app: node-app
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: node-app
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: node-app
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: node-container
|
||||||
|
image: kodekloud/centos-ssh-enabled:node
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: node-service
|
||||||
|
labels:
|
||||||
|
app: node-app
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
selector:
|
||||||
|
app: node-app
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
||||||
|
targetPort: 8080
|
||||||
|
nodePort: 30012
|
||||||
|
EOF
|
||||||
|
|
||||||
|
kubectl rollout status deployment/node-deployment
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The heredoc apply pattern
|
||||||
|
|
||||||
|
- **`kubectl apply -f -`** reads from **stdin**; `---` separates the two documents.
|
||||||
|
- **`<<'EOF'` (delimiter quoted)** keeps the manifest literal — the right default for k8s YAML.
|
||||||
|
|
||||||
|
### The Deployment
|
||||||
|
|
||||||
|
- **`image: kodekloud/centos-ssh-enabled:node`** and **`replicas: 2`** — the two hard
|
||||||
|
requirements. Name (`node-deployment`), container name (`node-container`), and labels
|
||||||
|
(`app: node-app`) are free choices, kept simple.
|
||||||
|
- **`containerPort: 8080`** — the Node app listens on 8080 inside the container, matching the
|
||||||
|
service's `targetPort`.
|
||||||
|
- **`selector.matchLabels: app: node-app`** equals the template labels — the Deployment↔pod link,
|
||||||
|
and what the Service selects on.
|
||||||
|
|
||||||
|
### The Service — the port mapping is the graded part
|
||||||
|
|
||||||
|
- **`type: NodePort`** exposes the app outside the cluster.
|
||||||
|
- **`selector: app: node-app`** targets the deployment's two pods by label and load-balances
|
||||||
|
across them.
|
||||||
|
- **Port mapping:**
|
||||||
|
- `port: 8080` — the Service's ClusterIP port.
|
||||||
|
- **`targetPort: 8080`** — the container port, exactly as required; must match where the app
|
||||||
|
listens.
|
||||||
|
- `nodePort: 30012` — the external port on each node, exactly as required (valid
|
||||||
|
`30000–32767` range).
|
||||||
|
|
||||||
|
Path: `<node-ip>:30012` → Service `:8080` → one of the two pods `:8080`.
|
||||||
|
|
||||||
|
### Ensuring all pods are Running (requirement 3)
|
||||||
|
|
||||||
|
`kubectl rollout status` blocks until **both** replicas are Ready. Only then is the deployment
|
||||||
|
fully available and the app reliably reachable through the Service — so wait for it to report
|
||||||
|
success before hitting the `NodeApp` button.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Both replicas running
|
||||||
|
kubectl get deployment node-deployment
|
||||||
|
kubectl get pods -l app=node-app
|
||||||
|
|
||||||
|
# Service on 30012 with two endpoints (one per pod)
|
||||||
|
kubectl get service node-service
|
||||||
|
kubectl get endpoints node-service
|
||||||
|
|
||||||
|
# App responding inside the cluster
|
||||||
|
kubectl exec deploy/node-deployment -- \
|
||||||
|
sh -c 'curl -sI http://localhost:8080 | head -n1' 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — deployment `READY 2/2`, both pods `Running`, `node-service` NodePort `8080:30012/TCP`
|
||||||
|
with **two** endpoints, and the app loading via the `NodeApp` button (`<node-ip>:30012`).
|
||||||
|
|
||||||
|
> If endpoints shows fewer than two IPs, a pod isn't Ready yet — give it a moment. Empty endpoints
|
||||||
|
> means the service selector doesn't match the pod labels.
|
||||||
126
kubernetes/level 3/task-1.md
Normal file
126
kubernetes/level 3/task-1.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
There is an application that needs to be deployed on Kubernetes cluster under Apache web server. The Nautilus application development team has asked the DevOps team to deploy it. We need to develop a template as per requirements mentioned below:
|
||||||
|
|
||||||
|
|
||||||
|
Create a namespace named as httpd-namespace-nautilus.
|
||||||
|
|
||||||
|
Create a deployment named as httpd-deployment-nautilus under newly created namespace. For the deployment use httpd image with latest tag only and remember to mention the tag i.e httpd:latest, and make sure replica counts are 2.
|
||||||
|
|
||||||
|
Create a service named as httpd-service-nautilus under same namespace to expose the deployment, nodePort should be 30004.
|
||||||
|
|
||||||
|
Note: The kubectl utility on the controlplane has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Apache on Kubernetes — namespace + deployment + service (`nautilus`)
|
||||||
|
|
||||||
|
An httpd Deployment (2 replicas) exposed via a NodePort Service, all inside a dedicated namespace.
|
||||||
|
One multi-document heredoc — no manifest file on disk.
|
||||||
|
|
||||||
|
## Apply (heredoc → kubectl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: httpd-namespace-nautilus
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: httpd-deployment-nautilus
|
||||||
|
namespace: httpd-namespace-nautilus
|
||||||
|
labels:
|
||||||
|
app: httpd
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: httpd
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: httpd
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: httpd-container
|
||||||
|
image: httpd:latest
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: httpd-service-nautilus
|
||||||
|
namespace: httpd-namespace-nautilus
|
||||||
|
labels:
|
||||||
|
app: httpd
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
selector:
|
||||||
|
app: httpd
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 80
|
||||||
|
nodePort: 30004
|
||||||
|
EOF
|
||||||
|
|
||||||
|
kubectl rollout status deployment/httpd-deployment-nautilus -n httpd-namespace-nautilus
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The heredoc apply pattern
|
||||||
|
|
||||||
|
- **`kubectl apply -f -`** reads from **stdin**; `---` separates the three documents, applied in
|
||||||
|
order so the **Namespace** exists before the Deployment and Service land in it.
|
||||||
|
- **`<<'EOF'` (delimiter quoted)** keeps the manifest literal — the right default for k8s YAML.
|
||||||
|
|
||||||
|
### Everything scoped to one namespace
|
||||||
|
|
||||||
|
All three objects carry `namespace: httpd-namespace-nautilus` (the Namespace defines it; the
|
||||||
|
Deployment and Service reference it). Creating the namespace first in the same stream avoids a
|
||||||
|
`namespaces "..." not found` error, and same-namespace scoping lets the Service select the
|
||||||
|
deployment's pods by label directly.
|
||||||
|
|
||||||
|
### The Deployment
|
||||||
|
|
||||||
|
- **`name: httpd-deployment-nautilus`**, **`image: httpd:latest`** (tag stated explicitly),
|
||||||
|
**`replicas: 2`** — all exactly as required.
|
||||||
|
- **`containerPort: 80`** — Apache serves on 80 inside the container.
|
||||||
|
- **`app: httpd`** label on the template — ties the Deployment to its pods and the Service to those
|
||||||
|
pods. Container name `httpd-container` is a free choice.
|
||||||
|
- **`selector.matchLabels: app: httpd`** equals the template labels — the Deployment↔pod link.
|
||||||
|
|
||||||
|
### The Service — targetPort 80
|
||||||
|
|
||||||
|
- **`type: NodePort`** exposes Apache outside the cluster.
|
||||||
|
- **`selector: app: httpd`** targets the deployment's two pods by label and load-balances across
|
||||||
|
them.
|
||||||
|
- **Port mapping:**
|
||||||
|
- `port: 80` — the Service's ClusterIP port.
|
||||||
|
- **`targetPort: 80`** — the container port; must be 80 to reach Apache.
|
||||||
|
- `nodePort: 30004` — the external port on each node, exactly as required (valid
|
||||||
|
`30000–32767` range).
|
||||||
|
|
||||||
|
Path: `<node-ip>:30004` → Service `:80` → one of the two pods `:80`.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Both replicas running in the namespace
|
||||||
|
kubectl get deployment httpd-deployment-nautilus -n httpd-namespace-nautilus
|
||||||
|
kubectl get pods -n httpd-namespace-nautilus -l app=httpd
|
||||||
|
|
||||||
|
# Service on 30004 with two endpoints
|
||||||
|
kubectl get service httpd-service-nautilus -n httpd-namespace-nautilus
|
||||||
|
kubectl get endpoints httpd-service-nautilus -n httpd-namespace-nautilus
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — deployment `READY 2/2`, both pods `Running`, `httpd-service-nautilus` NodePort
|
||||||
|
`80:30004/TCP` with **two** endpoints, and Apache reachable at `<node-ip>:30004`.
|
||||||
|
|
||||||
|
> Everything is in `httpd-namespace-nautilus` — keep `-n httpd-namespace-nautilus` on every command.
|
||||||
|
> If endpoints is empty, the service selector doesn't match the pod labels.
|
||||||
0
kubernetes/level 3/task-10.md
Normal file
0
kubernetes/level 3/task-10.md
Normal file
0
kubernetes/level 3/task-11.md
Normal file
0
kubernetes/level 3/task-11.md
Normal file
263
kubernetes/level 3/task-2.md
Normal file
263
kubernetes/level 3/task-2.md
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
The Nautilus DevOps team wants to deploy a PHP website on a Kubernetes cluster. They plan to use Apache as the web server and MySQL for the database. The team has already gathered the requirements and now wants to make the website live. More details can be found below:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
1) Create a ConfigMap named php-config containing the data variables_order = "EGPCS" for the php.ini file.
|
||||||
|
|
||||||
|
2) Create a Deployment named lamp-wp.
|
||||||
|
|
||||||
|
3) Within this Deployment, create two containers. The first container should be named httpd-php-container and utilize the image webdevops/php-apache:alpine-3-php7. The second container should be named mysql-container and use the image mysql:5.6. Mount the php-config ConfigMap in the httpd container at the location /opt/docker/etc/php/php.ini.
|
||||||
|
|
||||||
|
4) Note that secrets have already been created for the following MySQL-related values: MySQL root password, MySQL user, MySQL password, MySQL host, and MySQL database. These secrets are securely stored and can be accessed as needed.
|
||||||
|
|
||||||
|
5) Add the following environment variables for both containers: MYSQL_ROOT_PASSWORD, MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD, and MYSQL_HOST. Ensure that their values are sourced from the secrets created earlier. Please utilize the env field (do not use envFrom) to define the name-value pairs of the environment variables.
|
||||||
|
|
||||||
|
6) Create a NodePort type Service named lamp-service to expose the web application, specifying the NodePort as 30008.
|
||||||
|
|
||||||
|
7) Create a Service for MySQL named mysql-service, ensuring its port is set to 3306.
|
||||||
|
|
||||||
|
8) A file named /tmp/index.php is available on the jump-host.
|
||||||
|
|
||||||
|
a) Copy this file into the httpd container under the Apache document root at /app, replacing the dummy values for MySQL-related variables with the corresponding environment variables you have defined. Ensure that the MySQL-related details are not hardcoded in this file, and utilize environment variables to retrieve those values.
|
||||||
|
|
||||||
|
b) You should be able to access the index.php file through NodePort 30008. Upon accessing this page, the message Connected successfully should be displayed.
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Build LAMP WordPress Stack — `lamp-wp` + `php-config` + services
|
||||||
|
|
||||||
|
A two-container LAMP pod (Apache/PHP + MySQL), a ConfigMap for `php.ini`, two Services, and a PHP
|
||||||
|
page wired to the DB via env vars. Manifests applied via a multi-document heredoc, then the file
|
||||||
|
copied in.
|
||||||
|
|
||||||
|
## Step 0 — Confirm the secret names and keys (do this first)
|
||||||
|
|
||||||
|
The env vars source from pre-created Secrets. Verify their exact names/keys and adjust the manifest
|
||||||
|
if yours differ:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get secrets
|
||||||
|
for s in mysql-root-pass mysql-user-pass mysql-host mysql-db-url; do
|
||||||
|
echo "== $s =="; kubectl get secret "$s" -o jsonpath='{.data}' | tr ',' '\n'
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
This solution uses the standard mapping:
|
||||||
|
|
||||||
|
| Env var | Secret name | Key |
|
||||||
|
|---------|-------------|-----|
|
||||||
|
| `MYSQL_ROOT_PASSWORD` | `mysql-root-pass` | `password` |
|
||||||
|
| `MYSQL_DATABASE` | `mysql-db-url` | `database` |
|
||||||
|
| `MYSQL_USER` | `mysql-user-pass` | `username` |
|
||||||
|
| `MYSQL_PASSWORD` | `mysql-user-pass` | `password` |
|
||||||
|
| `MYSQL_HOST` | `mysql-host` | `host` |
|
||||||
|
|
||||||
|
## Step 1 — Apply ConfigMap + Deployment + Services (heredoc)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: php-config
|
||||||
|
data:
|
||||||
|
php.ini: |
|
||||||
|
variables_order = "EGPCS"
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: lamp-wp
|
||||||
|
labels:
|
||||||
|
app: lamp
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: lamp
|
||||||
|
tier: frontend
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: lamp
|
||||||
|
tier: frontend
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: httpd-php-container
|
||||||
|
image: webdevops/php-apache:alpine-3-php7
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
env:
|
||||||
|
- name: MYSQL_ROOT_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef: { name: mysql-root-pass, key: password }
|
||||||
|
- name: MYSQL_DATABASE
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef: { name: mysql-db-url, key: database }
|
||||||
|
- name: MYSQL_USER
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef: { name: mysql-user-pass, key: username }
|
||||||
|
- name: MYSQL_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef: { name: mysql-user-pass, key: password }
|
||||||
|
- name: MYSQL_HOST
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef: { name: mysql-host, key: host }
|
||||||
|
volumeMounts:
|
||||||
|
- name: php-config-volume
|
||||||
|
mountPath: /opt/docker/etc/php/php.ini
|
||||||
|
subPath: php.ini
|
||||||
|
- name: mysql-container
|
||||||
|
image: mysql:5.6
|
||||||
|
ports:
|
||||||
|
- containerPort: 3306
|
||||||
|
env:
|
||||||
|
- name: MYSQL_ROOT_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef: { name: mysql-root-pass, key: password }
|
||||||
|
- name: MYSQL_DATABASE
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef: { name: mysql-db-url, key: database }
|
||||||
|
- name: MYSQL_USER
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef: { name: mysql-user-pass, key: username }
|
||||||
|
- name: MYSQL_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef: { name: mysql-user-pass, key: password }
|
||||||
|
- name: MYSQL_HOST
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef: { name: mysql-host, key: host }
|
||||||
|
volumes:
|
||||||
|
- name: php-config-volume
|
||||||
|
configMap:
|
||||||
|
name: php-config
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: lamp-service
|
||||||
|
labels:
|
||||||
|
app: lamp
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
selector:
|
||||||
|
app: lamp
|
||||||
|
tier: frontend
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 80
|
||||||
|
nodePort: 30008
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: mysql-service
|
||||||
|
labels:
|
||||||
|
app: lamp
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: lamp
|
||||||
|
tier: frontend
|
||||||
|
ports:
|
||||||
|
- port: 3306
|
||||||
|
targetPort: 3306
|
||||||
|
EOF
|
||||||
|
|
||||||
|
kubectl rollout status deployment/lamp-wp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2 — Point index.php at the env vars (no hardcoded DB values)
|
||||||
|
|
||||||
|
Edit `/tmp/index.php` on the jump-host so the MySQL values come from `getenv(...)` instead of dummy
|
||||||
|
literals. The corrected connection block:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
$dbname = getenv('MYSQL_DATABASE');
|
||||||
|
$dbuser = getenv('MYSQL_USER');
|
||||||
|
$dbpass = getenv('MYSQL_PASSWORD');
|
||||||
|
$dbhost = getenv('MYSQL_HOST');
|
||||||
|
|
||||||
|
$conn = mysqli_connect($dbhost, $dbuser, $dbpass, $dbname);
|
||||||
|
if (!$conn) {
|
||||||
|
die("Connection failed: " . mysqli_connect_error());
|
||||||
|
}
|
||||||
|
echo "Connected successfully";
|
||||||
|
?>
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep the rest of the file as-is; only swap the four dummy assignments for the `getenv()` calls.
|
||||||
|
|
||||||
|
## Step 3 — Copy the file into the httpd container's document root (`/app`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POD=$(kubectl get pod -l app=lamp -o jsonpath='{.items[0].metadata.name}')
|
||||||
|
|
||||||
|
kubectl cp /tmp/index.php "$POD":/app/index.php -c httpd-php-container
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### ConfigMap → php.ini via subPath
|
||||||
|
|
||||||
|
`php-config` holds one key, `php.ini`, whose value is the directive `variables_order = "EGPCS"`.
|
||||||
|
It's mounted into the httpd container with **`subPath: php.ini`** at
|
||||||
|
`/opt/docker/etc/php/php.ini`. `subPath` mounts a **single file** into an existing directory rather
|
||||||
|
than replacing the whole directory — essential here, since `/opt/docker/etc/php/` contains other
|
||||||
|
files that must stay. Without `subPath`, the mount would hide everything else in that directory.
|
||||||
|
|
||||||
|
### Two containers, one pod
|
||||||
|
|
||||||
|
Apache/PHP and MySQL run as two containers in the **same pod**, sharing the pod's network namespace.
|
||||||
|
Both get the identical five env vars, each sourced from a Secret via **`valueFrom.secretKeyRef`** —
|
||||||
|
using the `env` field per the requirement (not `envFrom`, which would bulk-import a whole Secret
|
||||||
|
under its own key names). The MySQL container reads `MYSQL_*` to initialize the database and user;
|
||||||
|
the httpd container reads the same values so PHP can connect with matching credentials.
|
||||||
|
|
||||||
|
### The two Services
|
||||||
|
|
||||||
|
- **`lamp-service`** — `NodePort`, `targetPort: 80` (Apache's port), `nodePort: 30008`. This is the
|
||||||
|
public entry point: `<node-ip>:30008` → Apache `:80`.
|
||||||
|
- **`mysql-service`** — default ClusterIP, `port: 3306`. It gives MySQL a stable in-cluster DNS name
|
||||||
|
(`mysql-service`) so the app can reach the DB by name. Both Services select the same pod
|
||||||
|
(`app: lamp, tier: frontend`); each routes to the port relevant to its container.
|
||||||
|
|
||||||
|
Whatever the `mysql-host` Secret contains (e.g. `mysql-service` or `localhost`) is what
|
||||||
|
`MYSQL_HOST`/`$dbhost` becomes — so the app connects through the correct host without hardcoding it.
|
||||||
|
|
||||||
|
### Why the PHP file uses getenv()
|
||||||
|
|
||||||
|
Requirement 8a forbids hardcoding DB details. `getenv('MYSQL_HOST')` etc. pull the values from the
|
||||||
|
container's environment (the Secret-sourced env vars) at request time. So credentials live only in
|
||||||
|
Secrets and env — never in the file — and the page renders `Connected successfully` once the
|
||||||
|
connection succeeds. The webdevops php-apache image serves from **`/app`**, so `index.php` copied
|
||||||
|
there is reachable at the web root.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pod running with both containers
|
||||||
|
kubectl get pods -l app=lamp # READY 2/2
|
||||||
|
|
||||||
|
# php.ini mounted correctly
|
||||||
|
kubectl exec "$POD" -c httpd-php-container -- cat /opt/docker/etc/php/php.ini
|
||||||
|
|
||||||
|
# Services present
|
||||||
|
kubectl get svc lamp-service mysql-service
|
||||||
|
|
||||||
|
# The page returns the success message
|
||||||
|
curl -s http://<node-ip>:30008/index.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — pod `READY 2/2`, `php.ini` showing `variables_order = "EGPCS"`, both services present,
|
||||||
|
and the page printing **`Connected successfully`** at `<node-ip>:30008/index.php`.
|
||||||
|
|
||||||
|
> If it shows a connection error instead, MySQL 5.6 may still be initializing (first boot is slow) —
|
||||||
|
> retry after a minute. If it persists, re-check that the Secret names/keys in Step 0 match the
|
||||||
|
> manifest and that `mysql-host` resolves to a reachable host.
|
||||||
139
kubernetes/level 3/task-3.md
Normal file
139
kubernetes/level 3/task-3.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
There are some applications that need to be deployed on Kubernetes cluster and these apps have some pre-requisites where some configurations need to be changed before deploying the app container. Some of these changes cannot be made inside the images so the DevOps team has come up with a solution to use init containers to perform these tasks during deployment. Below is a sample scenario that the team is going to test first.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Create a Deployment named as ic-deploy-devops.
|
||||||
|
|
||||||
|
|
||||||
|
Configure spec as replicas should be 1, labels app should be ic-devops, template's metadata lables app should be the same ic-devops.
|
||||||
|
|
||||||
|
|
||||||
|
The initContainers should be named as ic-msg-devops, use image fedora with latest tag and use command '/bin/bash', '-c' and 'echo Init Done - Welcome to xFusionCorp Industries > /ic/media'. The volume mount should be named as ic-volume-devops and mount path should be /ic.
|
||||||
|
|
||||||
|
|
||||||
|
Main container should be named as ic-main-devops, use image fedora with latest tag and use command '/bin/bash', '-c' and 'while true; do cat /ic/media; sleep 5; done'. The volume mount should be named as ic-volume-devops and mount path should be /ic.
|
||||||
|
|
||||||
|
|
||||||
|
Volume to be named as ic-volume-devops and it should be an emptyDir type.
|
||||||
|
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
|
|
||||||
|
# Deployment with Init Container — `ic-deploy-devops`
|
||||||
|
|
||||||
|
An init container writes a message to a shared `emptyDir` volume; the main container then reads it
|
||||||
|
in a loop. This is the canonical init-container pattern. Applied inline via a heredoc — no manifest
|
||||||
|
file on disk.
|
||||||
|
|
||||||
|
## Apply (heredoc → kubectl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f - <<'EOF'
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: ic-deploy-devops
|
||||||
|
labels:
|
||||||
|
app: ic-devops
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: ic-devops
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: ic-devops
|
||||||
|
spec:
|
||||||
|
initContainers:
|
||||||
|
- name: ic-msg-devops
|
||||||
|
image: fedora:latest
|
||||||
|
command:
|
||||||
|
- /bin/bash
|
||||||
|
- -c
|
||||||
|
- "echo Init Done - Welcome to xFusionCorp Industries > /ic/media"
|
||||||
|
volumeMounts:
|
||||||
|
- name: ic-volume-devops
|
||||||
|
mountPath: /ic
|
||||||
|
containers:
|
||||||
|
- name: ic-main-devops
|
||||||
|
image: fedora:latest
|
||||||
|
command:
|
||||||
|
- /bin/bash
|
||||||
|
- -c
|
||||||
|
- "while true; do cat /ic/media; sleep 5; done"
|
||||||
|
volumeMounts:
|
||||||
|
- name: ic-volume-devops
|
||||||
|
mountPath: /ic
|
||||||
|
volumes:
|
||||||
|
- name: ic-volume-devops
|
||||||
|
emptyDir: {}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
kubectl rollout status deployment/ic-deploy-devops
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### The heredoc apply pattern
|
||||||
|
|
||||||
|
- **`kubectl apply -f -`** reads from **stdin**; nothing written to disk.
|
||||||
|
- **`<<'EOF'` (delimiter quoted)** keeps the manifest literal — the right default for k8s YAML.
|
||||||
|
|
||||||
|
### Init container vs. main container — the ordering guarantee
|
||||||
|
|
||||||
|
This is a **classic** init container (under `initContainers`, no `restartPolicy` override), which
|
||||||
|
behaves very differently from the main container:
|
||||||
|
|
||||||
|
- **`ic-msg-devops` (init)** runs **first, to completion**, before any main container starts. It
|
||||||
|
`echo`s the message into `/ic/media` and exits `0`. Because it's a one-shot task that finishes,
|
||||||
|
a classic init container is exactly right here — it does setup work, then gets out of the way.
|
||||||
|
- **`ic-main-devops` (main)** starts **only after** the init container succeeds. It then loops,
|
||||||
|
`cat`-ing `/ic/media` every 5 seconds. The `while true` keeps it running so the pod stays up.
|
||||||
|
|
||||||
|
The sequencing is guaranteed by Kubernetes: init containers complete before app containers begin.
|
||||||
|
That's what makes the pattern work — the file is written before the main container ever tries to
|
||||||
|
read it.
|
||||||
|
|
||||||
|
### The shared `emptyDir` volume
|
||||||
|
|
||||||
|
- **`ic-volume-devops` (`emptyDir: {}`)** is declared once and mounted at **`/ic` in both
|
||||||
|
containers**. `emptyDir` is created empty when the pod starts and shared by all its containers, so
|
||||||
|
the init container's write to `/ic/media` is visible to the main container at the same path. This
|
||||||
|
is the hand-off mechanism: init writes, main reads, through shared storage.
|
||||||
|
|
||||||
|
Both `volumeMounts` reference the same volume name (`ic-volume-devops`) at the same path — that's
|
||||||
|
what ties them to one storage location.
|
||||||
|
|
||||||
|
### Why the commands need `/bin/bash -c`
|
||||||
|
|
||||||
|
Each command is a shell one-liner (a redirect for the init, a loop for the main), so both run via
|
||||||
|
`/bin/bash -c`. The main container's `while true` loop is what keeps it in `Running` state — without
|
||||||
|
a long-running process, the `fedora` container would exit immediately and the pod would crash-loop.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Deployment ready
|
||||||
|
kubectl get deployment ic-deploy-devops
|
||||||
|
kubectl get pods -l app=ic-devops
|
||||||
|
|
||||||
|
# Init container completed (check pod init status)
|
||||||
|
kubectl get pod -l app=ic-devops \
|
||||||
|
-o jsonpath='{.items[0].status.initContainerStatuses[0].state}{"\n"}'
|
||||||
|
|
||||||
|
# Main container is printing the message from the shared volume
|
||||||
|
kubectl logs -l app=ic-devops -c ic-main-devops --tail=3
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected — deployment `READY 1/1`, the init container state showing `terminated`/`reason:
|
||||||
|
Completed`, and the main container's logs repeatedly printing
|
||||||
|
`Init Done - Welcome to xFusionCorp Industries`.
|
||||||
|
|
||||||
|
> If the pod is stuck in `Init:0/1`, the init container hasn't completed — check
|
||||||
|
> `kubectl logs -l app=ic-devops -c ic-msg-devops`. Unlike a native sidecar, a classic init
|
||||||
|
> container here is *meant* to finish, and it does (the echo exits immediately).
|
||||||
16
kubernetes/level 3/task-4.md
Normal file
16
kubernetes/level 3/task-4.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Assignment
|
||||||
|
|
||||||
|
The Nautilus DevOps team is working on a Kubernetes template to deploy a web application on the cluster. There are some requirements to create/use persistent volumes to store the application code, and the template needs to be designed accordingly. Please find more details below:
|
||||||
|
|
||||||
|
|
||||||
|
Create a PersistentVolume named as pv-xfusion. Configure the spec as storage class should be manual, set capacity to 4Gi, set access mode to ReadWriteOnce, volume type should be hostPath and set path to /mnt/itadmin (this directory is already created, you might not be able to access it directly, so you need not to worry about it).
|
||||||
|
|
||||||
|
Create a PersistentVolumeClaim named as pvc-xfusion. Configure the spec as storage class should be manual, request 2Gi of the storage, set access mode to ReadWriteOnce.
|
||||||
|
|
||||||
|
Create a pod named as pod-xfusion, mount the persistent volume you created with claim name pvc-xfusion at document root of the web server, the container within the pod should be named as container-xfusion using image nginx with latest tag only (remember to mention the tag i.e nginx:latest).
|
||||||
|
|
||||||
|
Create a node port type service named web-xfusion using node port 30008 to expose the web server running within the pod.
|
||||||
|
|
||||||
|
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
|
||||||
|
|
||||||
|
# Solution
|
||||||
0
kubernetes/level 3/task-5.md
Normal file
0
kubernetes/level 3/task-5.md
Normal file
0
kubernetes/level 3/task-6.md
Normal file
0
kubernetes/level 3/task-6.md
Normal file
0
kubernetes/level 3/task-7.md
Normal file
0
kubernetes/level 3/task-7.md
Normal file
0
kubernetes/level 3/task-8.md
Normal file
0
kubernetes/level 3/task-8.md
Normal file
0
kubernetes/level 3/task-9.md
Normal file
0
kubernetes/level 3/task-9.md
Normal file
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