docs: add Kubernetes CKS study notes

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

View File

@@ -0,0 +1,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 | 1520 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
- D12: 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
- D45: 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
- D13: drills on session-1 misses; Ultimate CKS mocks timed until <100 min clean
- D4: **killer.sh session 2** (the fluency retake — 90%+ or investigate)
- D56: 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.

View 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 48 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.

View 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

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

View 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

View 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."

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

View 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

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

View 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

View 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:`.

View 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).

View 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}'`.

View 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
(`3000032767`), 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.

View 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 (`3000032767`), 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`.

View 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`.

View 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:`.

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

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

View 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`.

View 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).

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

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

View 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`.

View 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}'`.

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

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

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

View 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 (`3000032767`), 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`).

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

View 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
(`3000032767`).
### `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.

View 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 `3000032767` 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.

View 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
`3000032767` 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.

View 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
`3000032767` 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.

View 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
`3000032767` 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.

View 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
`3000032767` 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.

View File

View File

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

View 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).

View 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

View File

View File

View File

View File

View File

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

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

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

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

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

View File

@@ -0,0 +1,421 @@
# CKA Drill Pack v1 — killer-style, self-validating
> Range: kind cluster `drills` (1 CP + 2 workers, Calico, Gateway API CRDs, metrics-server, helm).
> Protocol per drill: paste SETUP blindly → start clock → solve TASK → run VALIDATE → paste commands + time + validation output to Claude for grading.
> Rules: imperative first where a generator exists. Verify before declaring done. Budget is a ceiling, not a goal.
> Cleanup after each drill: `k delete ns <drill-ns> --wait=false` unless noted.
---
## K1 — NetworkPolicy: same-label, wrong-namespace trap — 8 min
**SETUP**
```bash
k create ns drill-np
k -n drill-np run db --image=nginx --labels=tier=db --port=80
k -n drill-np run api --image=nginx --labels=tier=api --port=80
k -n drill-np run web --image=nginx --labels=tier=web --port=80
k create ns drill-ext
k -n drill-ext run outsider --image=busybox --labels=tier=api -- sleep 3600
k -n drill-np wait --for=condition=Ready pod --all --timeout=60s
```
**TASK**
In Namespace `drill-np` create a NetworkPolicy `db-guard`: incoming traffic to Pods labeled `tier: db` allowed **only** on port 80 and **only** from Pods labeled `tier: api` **in the same Namespace**. All other ingress to db Pods blocked. Egress from db Pods unrestricted.
**Solution**
```bash
cat 1-netpol.yaml
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-guard
namespace: drill-np
spec:
podSelector:
matchLabels:
tier: db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
tier: api
ports:
- protocol: TCP
port: 80
```
**VALIDATE**
```bash
DB_IP=$(k -n drill-np get pod db -o jsonpath='{.status.podIP}')
k -n drill-np exec api -- curl -s -m 2 $DB_IP # nginx HTML (allowed)
k -n drill-np exec web -- curl -s -m 2 $DB_IP # timeout / exit 28 (blocked)
k -n drill-ext exec outsider -- wget -qO- -T 2 $DB_IP # timeout (blocked — same label, wrong ns)
```
Third check is the point: if it succeeds, your `from` clause selects by pod label without pinning the namespace.
---
## K2 — HPA: scale-up rate limiting — 7 min
**SETUP**
```bash
k create ns drill-hpa
k -n drill-hpa create deploy checkout --image=nginx --replicas=2
k -n drill-hpa set resources deploy checkout --requests=cpu=100m
```
**TASK**
Create HPA `checkout-hpa` for Deployment `checkout` in `drill-hpa`: min 2, max 6, target average CPU utilization 65%. Scale-**up** limited to at most 1 Pod per 120 seconds. (Up, not down — read your policy block twice.)
**Solution**
```bash
cat 2-hpa.yaml | k neat
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: checkout-hpa
namespace: drill-hpa
spec:
maxReplicas: 6
metrics:
- resource:
name: cpu
target:
averageUtilization: 60
type: Utilization
type: Resource
minReplicas: 2
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: checkout
```
**VALIDATE**
```bash
k -n drill-hpa get hpa checkout-hpa -o yaml | grep -A8 behavior
# scaleUp → policies: [{type: Pods, value: 1, periodSeconds: 120}]
k -n drill-hpa get hpa checkout-hpa -o jsonpath='{.spec.metrics[0].resource.target.averageUtilization}' # 65
k -n drill-hpa get hpa checkout-hpa # TARGETS: cpu%/65% (or <unknown> without metrics-server)
```
---
## K3 — HTTPRoute: header AND/OR semantics — 10 min
**SETUP**
```bash
# cluster prerequisities - GatewayApi
GWAPI=v1.3.0 # match to what your Cilium minor supports; check the Cilium docs for your version
# Standard channel: GatewayClass, Gateway, HTTPRoute, GRPCRoute, ReferenceGrant
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/${GWAPI}/standard-install.yaml
# Cilium additionally expects TLSRoute, which lives only in the experimental channel
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/${GWAPI}/config/crd/experimental/gateway.networking.k8s.io_tlsroutes.yaml
kubectl get crd | grep gateway.networking.k8s.io
# Step 2 — Enable Gateway API in Cilium
helm repo update cilium
helm upgrade cilium cilium/cilium -n kube-system \
--reuse-values \
--set gatewayAPI.enabled=true
# confirm the flag reached the ConfigMap
kubectl -n kube-system get cm cilium-config -o jsonpath='{.data.enable-gateway-api}{"\n"}' # true
## Step 3 — Restart operator and agents (REQUIRED — see Context)
kubectl -n kube-system rollout restart deploy/cilium-operator
kubectl -n kube-system rollout restart ds/cilium
kubectl -n kube-system rollout status deploy/cilium-operator --timeout=180s
kubectl -n kube-system rollout status ds/cilium --timeout=300s
kubectl create ns drill-gw
kubectl -n drill-gw create deploy legacy --image=hashicorp/http-echo -- /http-echo -text=legacy -listen=:8080
kubectl -n drill-gw create deploy beta --image=hashicorp/http-echo -- /http-echo -text=beta -listen=:8080
kubectl -n drill-gw expose deploy legacy --port=8080
kubectl -n drill-gw expose deploy beta --port=8080
kubectl -n drill-gw rollout status deploy/legacy deploy/beta
kubectl apply -f - <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata: {name: drill-gate, namespace: drill-gw}
spec:
gatewayClassName: cilium
listeners:
- {name: http, port: 80, protocol: HTTP, allowedRoutes: {namespaces: {from: Same}}}
EOF
kubectl -n drill-gw wait --for=condition=Programmed gateway/drill-gate --timeout=180s
kubectl -n drill-gw get gateway drill-gate
```
**TASK**
Create HTTPRoute `canary-route` in `drill-gw` attached to Gateway `drill-gate`:
1. `/app` (prefix) with header `X-Canary` exactly `true` → Service `beta:8080`
2. all other `/app``legacy:8080`
3. `/app` with `X-Canary: true` AND query param `debug=1` must ALSO reach `beta` — third rule or covered already? Justify in one sentence.
**VALIDATE** (spec-level — dummy class, no dataplane)
```bash
k -n drill-gw get httproute canary-route -o yaml
# 1: header+path in ONE match entry (AND), not two entries (OR)
# 2: header rule ordered/precedent over bare /app
k -n drill-gw get httproute canary-route -o jsonpath='{.spec.parentRefs[0].name}' # drill-gate
```
---
## K4 — Helm: pin, override, upgrade without losing values — 6 min
**SETUP**
```bash
helm repo add bitnami https://charts.bitnami.com/bitnami 2>/dev/null; helm repo update
```
**TASK**
Install `bitnami/nginx` as release `web-drill` in new namespace `drill-helm`, **exactly 2 replicas** set via CLI (no values file), chart version pinned one minor **behind** latest. Print the user-supplied values of the deployed release. Upgrade to latest chart version **keeping** the replica override.
**VALIDATE**
```bash
helm list -n drill-helm # pinned version, then latest after upgrade
helm get values web-drill -n drill-helm # replicaCount: 2 — must survive the upgrade
k -n drill-helm get deploy # READY 2/2
helm history web-drill -n drill-helm # rev1 install, rev2 upgrade
```
Trap: "keeping your override" — there is a right flag and a wrong assumption.
---
## K5 — Kustomize: overlay surgery (extended) — 14 min
**SETUP**
```bash
mkdir -p /tmp/drill-kz/base /tmp/drill-kz/overlays/prod
cat > /tmp/drill-kz/base/deploy.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: portal
labels: {app: portal}
spec:
replicas: 1
selector: {matchLabels: {app: portal}}
template:
metadata: {labels: {app: portal}}
spec:
containers:
- name: portal
image: nginx:1.25
envFrom:
- configMapRef: {name: portal-config}
EOF
cat > /tmp/drill-kz/base/svc.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
name: portal
labels: {app: portal}
spec:
selector: {app: portal}
ports:
- {port: 80, targetPort: 80}
EOF
cat > /tmp/drill-kz/base/kustomization.yaml <<'EOF'
resources: [deploy.yaml, svc.yaml]
configMapGenerator:
- name: portal-config
literals: [APP_MODE=basic]
EOF
```
**TASK**
Build overlay `prod` at `/tmp/drill-kz/overlays/prod` that, without touching base, does ALL of the following:
1. Sets namespace `drill-prod` on everything.
2. Adds name prefix `prod-` to every resource.
3. Adds common label `env: prod` — must land on the Deployment's metadata, its pod template, **and** its selector/Service selector (labels + selectors stay consistent).
4. Bumps the image to `nginx:1.27` (use the `images` transformer, not a patch).
5. Scales replicas to 3 (use the `replicas` transformer, not a patch).
6. **Extends** the `portal-config` ConfigMap with `LOG_LEVEL=debug` while **keeping** `APP_MODE=basic` — this must be a generator merge, not a redefinition.
7. Adds a **new** `portal-secret` Secret generator (literal `API_KEY=s3cr3t`) and wires it into the container's `envFrom` via a **JSON 6902** patch — reference the secret by its plain generator name and let kustomize's name-reference fixup resolve the real (hashed, prefixed) name for you.
8. Adds container resource limits `cpu: 200m`, `memory: 128Mi` via the same or a second JSON 6902 patch (`resources` isn't set in base — the op must `add`, not `replace`).
Create namespace `drill-prod`, apply the overlay with `-k`.
**VALIDATE**
```bash
kubectl kustomize /tmp/drill-kz/overlays/prod # eyeball everything before applying — 4 objects: Deployment, Service, ConfigMap, Secret
k -n drill-prod get deploy prod-portal -o jsonpath='{.spec.replicas} {.spec.template.spec.containers[0].image}'
# 3 nginx:1.27
k -n drill-prod get deploy prod-portal -o jsonpath='{.metadata.labels.env} {.spec.selector.matchLabels.env} {.spec.template.metadata.labels.env}'
# prod prod prod (object label + selector + template label all consistent)
CM=$(k -n drill-prod get cm -o name | grep prod-portal-config)
k -n drill-prod get $CM -o jsonpath='{.data}'
# {"APP_MODE":"basic","LOG_LEVEL":"debug"} — both keys present, merge not replace
k -n drill-prod get secret -o name | grep prod-portal-secret # generated, hashed, prefixed
k -n drill-prod get deploy prod-portal -o jsonpath='{.spec.template.spec.containers[0].envFrom}'
# two entries — configMapRef and secretRef both pointing at the REAL hashed+prefixed names
# if secretRef.name literally reads "portal-secret", your patch bypassed kustomize's name-reference transformer
k -n drill-prod get deploy prod-portal -o jsonpath='{.spec.template.spec.containers[0].resources.limits}'
# {"cpu":"200m","memory":"128Mi"}
```
Trap: item 7 is the point of the drill — patch against the *pre-transform* name and trust kustomize to rewrite it everywhere; hand-typing the hashed name works today and silently breaks the next time a literal changes.
---
## K6 — CRD + CR: author the schema — 10 min
**SETUP** — none. Blank slate is the drill.
**TASK**
Create CRD `backups.data.example.com`: namespaced, kind `Backup`, plural `backups`, shortName `bk`, version `v1` (served+storage). Schema: `spec.source` (string, required), `spec.retentionDays` (integer, 130). Then create a `Backup` named `nightly` in namespace `drill-crd` with source `pg-main`, retentionDays 7. Then prove validation works: attempt retentionDays 99 and capture the rejection.
**VALIDATE**
```bash
k api-resources | grep backups # data.example.com, bk, Backup
k explain backup.spec # both fields, with types
k -n drill-crd get bk nightly -o jsonpath='{.spec.retentionDays}' # 7
# the 99 attempt must fail with a schema validation error — paste the error line
```
---
## K7 — RBAC + SA + API curl: the 403 chain — 10 min
**SETUP**
```bash
k create ns drill-rbac
k -n drill-rbac create sa auditor
k -n drill-rbac run client --image=nginx:1-alpine --overrides='{"spec":{"serviceAccountName":"auditor"}}'
```
**TASK**
1. Grant SA `auditor` permission to **get and list** ConfigMaps in `drill-rbac` only — imperative commands only, no YAML files.
2. Prove with `kubectl auth can-i` (positive AND negative: it must NOT list secrets).
3. Exec into `client` and curl the ConfigMaps list endpoint with the mounted token — capture the HTTP response kind.
4. Curl the Secrets endpoint the same way — capture the 403.
**VALIDATE**
```bash
k auth can-i list configmaps --as=system:serviceaccount:drill-rbac:auditor -n drill-rbac # yes
k auth can-i list secrets --as=system:serviceaccount:drill-rbac:auditor -n drill-rbac # no
# curl 1 → "kind":"ConfigMapList"
# curl 2 → "kind":"Status","code":403
```
---
## K8 — Scheduling: taints, tolerations, spread — 9 min
**SETUP**
```bash
k taint node dev-cilium-worker dedicated=batch:NoSchedule
k label node dev-cilium-worker pool=batch
k label node dev-cilium-worker2 pool=web
```
**TASK**
1. Create deployment `batch-runner` (image `busybox`, command `sleep 3600`, 2 replicas) in new ns `drill-sched` that runs **only** on `drills-worker` — it must tolerate the taint AND be constrained to that node via its label (both mechanisms, and know why one alone is insufficient).
2. Create pod `web-pin` (nginx) that must land on `drills-worker2` using nodeAffinity (no nodeName).
**VALIDATE**
```bash
k -n drill-sched get pods -o wide # both batch-runner pods on drills-worker, web-pin on drills-worker2
k -n drill-sched get deploy batch-runner -o yaml | grep -A6 tolerations # dedicated=batch:NoSchedule tolerated
k -n drill-sched get pod web-pin -o yaml | grep -A10 affinity # nodeAffinity on pool=web
```
Question to answer in your paste: what happens with toleration but no selector? (One sentence.)
**CLEANUP** (mandatory — taints leak into later drills)
```bash
k taint node drills-worker dedicated=batch:NoSchedule-
k label node drills-worker pool- ; k label node drills-worker2 pool-
```
---
## K9 — Storage: PV/PVC binding chain — 8 min
**SETUP**
```bash
k apply -f - <<'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: {name: drill-manual}
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
EOF
```
**TASK**
1. Create PV `pv-drill`: 1Gi, `ReadWriteOnce`, storageClassName `drill-manual`, hostPath `/tmp/pv-drill`.
2. Create PVC `pvc-drill` in new ns `drill-store`: request 500Mi, same class, RWO.
3. Explain (one sentence) why the PVC stays `Pending` right now — this is not an error.
4. Create pod `store-user` (nginx) mounting the PVC at `/data`; confirm the bind flips.
**VALIDATE**
```bash
k -n drill-store get pvc pvc-drill # Pending BEFORE pod, Bound AFTER
k get pv pv-drill # Bound, CLAIM drill-store/pvc-drill
k -n drill-store exec store-user -- sh -c 'echo ok > /data/probe && cat /data/probe' # ok
```
---
## K10 — Native sidecar + output formatting — 8 min
**SETUP** — none.
**TASK**
1. In new ns `drill-side`, create deployment `tracked-app` (1 replica): main container `app` (busybox) appends the date to `/var/log/app/beat.log` every 5s; **native sidecar** `tail-agent` (busybox) — init container with `restartPolicy: Always` — tails that file. Shared emptyDir at `/var/log/app`.
2. One command: print for every pod in `drill-side` the columns `NAME`, `QOS`, `NODE`, sorted by name, written to `/tmp/drill-side.txt`.
**VALIDATE**
```bash
k -n drill-side get pod -o jsonpath='{.items[0].spec.initContainers[0].restartPolicy}' # Always
k -n drill-side logs deploy/tracked-app -c tail-agent | tail -3 # dated heartbeats flowing
cat /tmp/drill-side.txt # header row + one line: name, QoS class, node
```
---
## Grading rubric (what Claude checks on every paste-back)
1. Correct end state (validation output matches expected)
2. Path efficiency — generator/patch/docs-copy vs hand-written YAML
3. Verification discipline — did you run VALIDATE before declaring done
4. Time vs budget
5. Trap detection — each drill has one; naming it explicitly earns full marks
## Score log
| Drill | Date | Time | Clean? | Trap caught? | Notes |
|---|---|---|---|---|---|
| K1 | | | | | |
| K2 | | | | | |
| K3 | | | | | |
| K4 | | | | | |
| K5 | | | | | |
| K6 | | | | | |
| K7 | | | | | |
| K8 | | | | | |
| K9 | | | | | |
| K10 | | | | | |

View 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
# --------
```

View 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

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

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

View File