"""CKAD lessons: guided labs, one per curriculum objective, on a cluster of your own and without a clock. A lesson explains, then walks you through steps that you do on its host; each step has a check, a hint and the commands that do it. It ends by sending you to the objective's timed drill. Lessons write nothing to the history or the scores - only a "lesson done" mark on the objective. ./ksim lesson the lessons of this track, and which you have done ./ksim lesson 2.1 start the lesson of objective 2.1 (id or key) ./ksim lesson check check the step you are on; the next one follows ./ksim lesson audit [OBJ..] prove lessons: every step fails before its commands and passes after A lesson's host is named after its id - lesson 9201 runs on `ckad9201` - which never clashes with a task's. Its files go under {WS}/lesson-/. A step's `show` commands are what the learner types; the audit runs exactly them, as `candidate`, so what a lesson shows always works. Pilot (2026-10-16): the Application Deployment domain, 2.1 to 2.4. """ from sim.bank import NS, step # ---- 2.1 blue/green and canary --------------------------------------------------------------- _WEB = r""" apiVersion: apps/v1 kind: Deployment metadata: name: web-%{v} labels: {app: web, version: %{v}} spec: replicas: 2 selector: matchLabels: {app: web, version: %{v}} template: metadata: labels: {app: web, version: %{v}} spec: containers: - name: web image: busybox:1.36 command: [sh, -c, 'mkdir -p /www && echo %{v} > /www/index.html && exec httpd -f -p 80 -h /www'] ports: - containerPort: 80 """ # a request through Service web from Pod client, retried while endpoints settle _ASK = r""" ask() { local i; for i in 1 2 3 4 5 6; do [ "$($K -n shop exec client -- wget -qO- -T 3 web 2>/dev/null)" = "$1" ] && return 0; sleep 2; done; return 1; } """ L21 = { "id": 9201, "objective": "2.1", "workers": 0, "title": "Blue/green and canary with plain Deployments", "intro": r"""A Deployment replaces its Pods by rolling them over. Sometimes you want more control than that: run the new version **beside** the old one, try it, and decide when it gets the traffic. Nothing beyond plain Deployments and a Service is needed, because the **Service's selector** decides which Pods receive requests. **Blue/green**: two Deployments, one Service. Changing the Service's selector moves all traffic at once, and changing it back is your rollback: ``` ┌─▶ web-blue (blue) Service web ─selector─┤ └╌▶ web-green (green) ``` **Canary**: let the Service select **both** versions, and set the split with replica counts. With 4 stable Pods and 1 canary Pod, about one request in five reaches the canary. In Namespace `shop` you will find Deployment `web-blue` (each of its Pods answers `blue`), Service `web` in front of it, and a Pod `client` to send requests from.""", "setup": NS + r""" mkns shop $K -n shop apply -f - >/dev/null </dev/null $K -n shop rollout status deploy web-blue --timeout=180s >/dev/null $K -n shop wait --for=condition=Ready pod/client --timeout=180s >/dev/null mkdir -p "$WS/lesson-2.1" """, "steps": [ step("Ask the Service who answers", r"""Before you change anything, see what users get today. The Pod `client` has `wget`, and inside the Namespace a Service is reachable by its name: ``` kubectl -n shop exec client -- wget -qO- web ``` Save the answer in `{WS}/lesson-2.1/before.txt`.""", check=r"""tr -d '\r' < "$WS/lesson-2.1/before.txt" 2>/dev/null | grep -qx blue""", hint=r"""`kubectl exec POD -- COMMAND` runs a command in a running Pod; `wget -qO-` prints the page instead of saving it. Pipe it through `tee FILE`, or redirect with `> FILE`.""", show=r"""kubectl -n shop exec client -- wget -qO- web | tee {WS}/lesson-2.1/before.txt"""), step("Start green beside blue", r"""Create Deployment `web-green` next to `web-blue`: 2 replicas, the same container, except that each Pod answers `green`. Its Pods carry the labels `app=web` and `version=green`. Copying blue's manifest is the quickest start: `kubectl -n shop get deploy web-blue -o yaml`. Change the name, the `version` label everywhere and the word `blue` in the command, and leave out `status`, `uid`, `resourceVersion` and `creationTimestamp`. Wait until both Pods are ready. The Service still selects `version=blue`, so no user sees green yet: you can try it first.""", check=r""" eq "$(j -n shop deploy web-green '{.status.availableReplicas}')" 2 \ && eq "$(j -n shop deploy web-green '{.spec.template.metadata.labels.app}')" web \ && eq "$(j -n shop deploy web-green '{.spec.template.metadata.labels.version}')" green || exit 1 ip=$(j -n shop pods -l app=web,version=green '{.items[0].status.podIP}') [ -n "$ip" ] && eq "$($K -n shop exec client -- wget -qO- -T 3 "$ip" 2>/dev/null)" green""", hint=r"""Labels appear three times in a Deployment: `metadata.labels` (optional), `spec.selector.matchLabels` and `spec.template.metadata.labels` - the selector must match the template. `kubectl -n shop rollout status deploy web-green` waits until it is ready, and `kubectl -n shop get pods -l version=green -o wide` shows the Pod IPs to try.""", show=r"""kubectl -n shop apply -f - <<'EOF' """ + _WEB.replace("%{v}", "green").strip("\n") + r""" EOF kubectl -n shop rollout status deploy web-green kubectl -n shop get pods -l app=web --show-labels"""), step("Cut over to green", r"""Point Service `web` at green. Changing the selector **is** the cutover: within a second every new request goes to green. Careful: `kubectl set selector` replaces the **whole** selector, so name both `app` and `version`. Then ask the Service again from `client` - the answer should be `green`.""", check=_ASK + r""" eq "$(j -n shop svc web '{.spec.selector.app}')" web && eq "$(j -n shop svc web '{.spec.selector.version}')" green && ask green""", hint=r"""`kubectl set selector svc web 'app=web,version=green'` - or `kubectl patch svc web -p '{"spec":{"selector":{"version":"green"}}}'`, which merges and keeps `app`. `kubectl -n shop get svc web -o wide` shows the selector.""", show=r"""kubectl -n shop set selector svc web 'app=web,version=green' kubectl -n shop get svc web -o wide kubectl -n shop exec client -- wget -qO- web"""), step("Roll back in one command", r"""Say green misbehaves. Rolling back a blue/green release is the same move in reverse: point the Service at blue again. Blue never stopped running, so this is instant - no new rollout, no image to pull. Switch back, and ask the Service once more.""", check=_ASK + r""" eq "$(j -n shop svc web '{.spec.selector.app}')" web && eq "$(j -n shop svc web '{.spec.selector.version}')" blue && ask blue""", hint=r"""The command you used for the cutover, with `version=blue`.""", show=r"""kubectl -n shop set selector svc web 'app=web,version=blue' kubectl -n shop exec client -- wget -qO- web"""), step("Canary: one Service, both versions", r"""Now a canary. Let Service `web` select **every** Pod of the app, blue and green, by dropping `version` from its selector. Requests spread over all ready endpoints, so with 2 blue and 2 green Pods about half of them reach green. Ask the Service a few times and watch the answers change.""", check=r""" eq "$(j -n shop svc web '{.spec.selector.app}')" web && [ -z "$(j -n shop svc web '{.spec.selector.version}')" ] || exit 1 for i in 1 2 3 4 5 6; do n=$($K -n shop get endpointslices -l kubernetes.io/service-name=web -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{"\n"}{end}' 2>/dev/null | grep -c .) [ "$n" -eq 4 ] && exit 0; sleep 2 done; exit 1""", hint=r"""`kubectl set selector svc web 'app=web'` leaves only `app`. `kubectl -n shop get endpointslices -l kubernetes.io/service-name=web` lists the Pods that receive traffic.""", show=r"""kubectl -n shop set selector svc web 'app=web' kubectl -n shop get endpointslices -l kubernetes.io/service-name=web for i in 1 2 3 4 5 6 7 8; do kubectl -n shop exec client -- wget -qO- web; done"""), step("Weight the canary to 20%", r"""Half the traffic is too much for a canary. The split follows the replica counts, so run **4** blue Pods and **1** green Pod: about one request in five reaches green. When green has proved itself you would scale it up and blue down, then remove blue - that is the whole canary.""", check=r""" eq "$(j -n shop deploy web-blue '{.status.availableReplicas}')" 4 && eq "$(j -n shop deploy web-green '{.spec.replicas}')" 1 \ && eq "$(j -n shop deploy web-green '{.status.availableReplicas}')" 1 \ && eq "$(j -n shop svc web '{.spec.selector.app}')" web && [ -z "$(j -n shop svc web '{.spec.selector.version}')" ]""", hint=r"""`kubectl scale deploy NAME --replicas=N`, once for each Deployment.""", show=r"""kubectl -n shop scale deploy web-blue --replicas=4 kubectl -n shop scale deploy web-green --replicas=1 kubectl -n shop rollout status deploy web-blue kubectl -n shop get deploy"""), ], "outro": r"""You did a blue/green cutover, an instant rollback and a weighted canary with nothing but Deployments, labels and one Service. **For the exam** - The Service's selector is the switch. `kubectl get svc NAME -o wide` shows it; `kubectl get endpointslices -l kubernetes.io/service-name=NAME` shows who receives traffic. - `kubectl set selector` replaces the whole selector; `kubectl patch` merges into it. - Keep blue running until a task says otherwise: it is your rollback. - A canary's share is its Pods divided by all the Pods behind the Service. Next, prove it against the clock: **drill 2.1** gives you this objective's tasks with a timer, and no solution until a task is graded.""", } # ---- 2.2 rolling updates and rollbacks ------------------------------------------------------- L22 = { "id": 9202, "objective": "2.2", "workers": 0, "title": "Rolling updates, a bad release, and the way back", "intro": r"""A Deployment never edits a running Pod. Change its Pod template - a new image, a new environment variable - and it creates a new ReplicaSet, then moves Pods over from the old one a few at a time. Two numbers decide how: ``` maxSurge extra Pods during an update maxUnavailable Pods that may be missing ``` Each template change is a **revision**, and you can list revisions and go back to one. In Namespace `rollout`, Deployment `api` runs `nginx:1.25` with 4 replicas.""", "setup": NS + r""" mkns rollout $K -n rollout create deploy api --image=nginx:1.25 --replicas=4 >/dev/null $K -n rollout rollout status deploy api --timeout=180s >/dev/null 2>&1 mkdir -p "$WS/lesson-2.2" """, "steps": [ step("Read the update strategy", r"""Find the Deployment's `maxSurge` and `maxUnavailable`, and write them separated by a space to `{WS}/lesson-2.2/strategy.txt` - for example `25% 25%`.""", check=r"""eq "$(tr -d '\r\n' < "$WS/lesson-2.2/strategy.txt" 2>/dev/null | tr -s ' ')" "25% 25%" """, hint=r"""Both live under `.spec.strategy.rollingUpdate`. `kubectl explain deploy.spec.strategy.rollingUpdate` describes them, and `-o jsonpath='{...}'` prints only the values.""", show=r"""kubectl -n rollout get deploy api -o jsonpath='{.spec.strategy.rollingUpdate.maxSurge} {.spec.strategy.rollingUpdate.maxUnavailable}' > {WS}/lesson-2.2/strategy.txt cat {WS}/lesson-2.2/strategy.txt"""), step("Never drop below four Pods", r"""25% of 4 Pods means one Pod may be missing during an update. Make updates add a Pod before they remove one: set `maxUnavailable` to `0` and `maxSurge` to `1`.""", check=r"""eq "$(j -n rollout deploy api '{.spec.strategy.rollingUpdate.maxUnavailable}')" 0 && eq "$(j -n rollout deploy api '{.spec.strategy.rollingUpdate.maxSurge}')" 1""", hint=r"""A merge patch with `kubectl patch deploy api -p '{"spec":{"strategy":{...}}}'`, or `kubectl edit deploy api`.""", show=r"""kubectl -n rollout patch deploy api -p '{"spec":{"strategy":{"rollingUpdate":{"maxSurge":1,"maxUnavailable":0}}}}' kubectl -n rollout get deploy api -o jsonpath='{.spec.strategy}'; echo"""), step("Roll out nginx 1.27, with a reason", r"""Update the container `nginx` to image `nginx:1.27`, and record why with the annotation `kubernetes.io/change-cause` set to `nginx 1.27`: `kubectl rollout history` shows it in the CHANGE-CAUSE column. Follow the update with `kubectl rollout status` until it finishes.""", check=r""" eq "$(j -n rollout deploy api '{.spec.template.spec.containers[0].image}')" nginx:1.27 \ && eq "$(j -n rollout deploy api '{.metadata.annotations.kubernetes\.io/change-cause}')" "nginx 1.27" \ && eq "$(j -n rollout deploy api '{.status.updatedReplicas}')" 4 && eq "$(j -n rollout deploy api '{.status.availableReplicas}')" 4 \ && eq "$(j -n rollout deploy api '{.status.replicas}')" 4""", hint=r"""`kubectl set image deploy/api nginx=nginx:1.27` - the name before `=` is the **container's**. Then `kubectl annotate deploy/api kubernetes.io/change-cause='nginx 1.27'`.""", show=r"""kubectl -n rollout set image deploy/api nginx=nginx:1.27 kubectl -n rollout annotate deploy/api kubernetes.io/change-cause='nginx 1.27' --overwrite kubectl -n rollout rollout status deploy/api kubectl -n rollout rollout history deploy/api"""), step("Ship a bad release", r"""Now a mistake: update to image `nginx:9.99-nope`, which does not exist. Run `kubectl rollout status` with `--timeout=20s` and look at the Pods: one new Pod cannot pull its image, and because of `maxUnavailable: 0` all four old Pods keep serving. A rollout that cannot finish does not take your app down - it stalls.""", check=r""" eq "$(j -n rollout deploy api '{.spec.template.spec.containers[0].image}')" nginx:9.99-nope || exit 1 eq "$(j -n rollout deploy api '{.status.availableReplicas}')" 4 || exit 1 for i in $(seq 1 20); do $K -n rollout get pods -o jsonpath='{range .items[*]}{.status.containerStatuses[0].state.waiting.reason}{"\n"}{end}' 2>/dev/null \ | grep -Eq 'ErrImagePull|ImagePullBackOff' && exit 0 sleep 3 done; exit 1""", hint=r"""`kubectl set image` once more. `kubectl get pods` shows `ErrImagePull` or `ImagePullBackOff`, and `kubectl describe pod NAME` says why.""", show=r"""kubectl -n rollout set image deploy/api nginx=nginx:9.99-nope kubectl -n rollout rollout status deploy/api --timeout=20s kubectl -n rollout get pods"""), step("Roll back", r"""Undo the bad release: `kubectl rollout undo` returns to the previous revision, `nginx:1.27`, and the stuck Pod goes away. Wait until the rollout is complete.""", check=r""" eq "$(j -n rollout deploy api '{.spec.template.spec.containers[0].image}')" nginx:1.27 \ && eq "$(j -n rollout deploy api '{.status.updatedReplicas}')" 4 && eq "$(j -n rollout deploy api '{.status.availableReplicas}')" 4 \ && eq "$(j -n rollout deploy api '{.status.replicas}')" 4""", hint=r"""`kubectl rollout undo deploy/api` goes back one revision; `--to-revision=N` goes back to any one that `kubectl rollout history` lists.""", show=r"""kubectl -n rollout rollout undo deploy/api kubectl -n rollout rollout status deploy/api"""), step("Read the history right", r"""List the revisions with `kubectl rollout history deploy/api`. The undo did not go back to revision 2: it made the 1.27 template the **newest** revision, and revision 2 is gone from the list. Write the current revision number to `{WS}/lesson-2.2/revision.txt`.""", check=r""" cur=$(j -n rollout deploy api '{.metadata.annotations.deployment\.kubernetes\.io/revision}') [ -n "$cur" ] && eq "$(tr -dc '0-9' < "$WS/lesson-2.2/revision.txt" 2>/dev/null)" "$cur" """, hint=r"""The last line of `kubectl rollout history` - or the Deployment's annotation `deployment.kubernetes.io/revision`.""", show=r"""kubectl -n rollout rollout history deploy/api kubectl -n rollout get deploy api -o jsonpath='{.metadata.annotations.deployment\.kubernetes\.io/revision}' > {WS}/lesson-2.2/revision.txt cat {WS}/lesson-2.2/revision.txt"""), ], "outro": r"""You read and tuned an update strategy, rolled out a new image with a recorded reason, watched a broken release stall without an outage, and rolled it back. **For the exam** - `kubectl set image deploy/NAME CONTAINER=IMAGE`: the **container** name goes before `=`. - `kubectl rollout status` waits, `rollout history` lists revisions, `rollout undo [--to-revision=N]` goes back, and `rollout pause` / `resume` gather several changes into one rollout. - `maxUnavailable: 0` with a `maxSurge` keeps full capacity during an update, and a broken image only stalls it. - The `kubernetes.io/change-cause` annotation is what `rollout history` shows as CHANGE-CAUSE (`--record` no longer exists). Next, prove it against the clock: **drill 2.2**.""", } # ---- 2.3 Helm ----------------------------------------------------------------------------------- L23 = { "id": 9203, "objective": "2.3", "workers": 0, "title": "Helm: install, upgrade, roll back", "intro": r"""Helm installs an application from a **chart**: templates plus default values. One installation is a **release**, and every change to a release - an upgrade or a rollback - is a new **revision** that you can list and return to. ``` chart ── helm install shop ──▶ revision 1 ── helm upgrade shop ──▶ revision 2, 3 .. ``` In the exam a chart usually comes from a repository (`helm repo add`, `helm search repo`). Here it is local, which works the same: a chart directory or a packaged `.tgz` can go anywhere a chart name can. In `{WS}/lesson-2.3` you will find a chart `webapp` 0.1.0 as a directory, `chart/`, and version 0.2.0 packaged as `webapp-0.2.0.tgz`.""", "setup": r""" d="$WS/lesson-2.3"; rm -rf "$d"; mkdir -p "$d" src=$(mktemp -d) for v in 0.1.0 0.2.0; do c="$src/$v/webapp"; mkdir -p "$c/templates" cat > "$c/Chart.yaml" < "$c/values.yaml" <<'EOF' replicaCount: 1 image: repository: nginx tag: "1.27" service: type: ClusterIP port: 80 EOF cat > "$c/templates/deployment.yaml" <<'EOF' apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }}-webapp labels: {app.kubernetes.io/instance: {{ .Release.Name }}, helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}} spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: {app.kubernetes.io/instance: {{ .Release.Name }}} template: metadata: labels: {app.kubernetes.io/instance: {{ .Release.Name }}} spec: containers: - name: web image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" ports: - containerPort: 80 EOF cat > "$c/templates/service.yaml" <<'EOF' apiVersion: v1 kind: Service metadata: name: {{ .Release.Name }}-webapp spec: type: {{ .Values.service.type }} selector: {app.kubernetes.io/instance: {{ .Release.Name }}} ports: - port: {{ .Values.service.port }} targetPort: 80 EOF done cp -r "$src/0.1.0/webapp" "$d/chart" helm package "$src/0.2.0/webapp" -d "$d" >/dev/null rm -rf "$src" command -v helm >/dev/null || echo "warn: helm is not installed on this machine - the checks need it" """, "steps": [ step("Look inside before you install", r"""See what a chart would do without touching the cluster. `helm show values` lists its knobs and their defaults; `helm template` renders the manifests Helm would apply. Render the chart in `{WS}/lesson-2.3/chart` as release `shop` into `{WS}/lesson-2.3/rendered.yaml`, and read it.""", check=r"""f="$WS/lesson-2.3/rendered.yaml"; grep -q 'kind: Deployment' "$f" 2>/dev/null && grep -q 'kind: Service' "$f" && grep -q 'name: shop-webapp' "$f" """, hint=r"""`helm template RELEASE CHART` prints to stdout: redirect it with `> FILE`.""", show=r"""helm show values {WS}/lesson-2.3/chart helm template shop {WS}/lesson-2.3/chart > {WS}/lesson-2.3/rendered.yaml cat {WS}/lesson-2.3/rendered.yaml"""), step("Install it", r"""Install the chart as release `shop` into Namespace `charts` - let Helm create the Namespace - with **2** replicas instead of the default 1.""", check=r""" helm -n charts status shop -o json 2>/dev/null | python3 -c 'import sys,json; sys.exit(0 if json.load(sys.stdin)["info"]["status"]=="deployed" else 1)' || exit 1 eq "$(j -n charts deploy shop-webapp '{.status.availableReplicas}')" 2""", hint=r"""`helm install RELEASE CHART -n NAMESPACE --create-namespace --set replicaCount=2`. `helm list -n charts` shows the release.""", show=r"""helm -n charts install shop {WS}/lesson-2.3/chart --create-namespace --set replicaCount=2 helm -n charts list kubectl -n charts rollout status deploy shop-webapp"""), step("Upgrade with a values file", r"""`--set` is fine for one value; for several, a values file. Write `{WS}/lesson-2.3/prod-values.yaml` with `replicaCount: 3` and a Service of type `NodePort`, and upgrade `shop` with it. `helm history` then lists a second revision.""", check=r""" grep -Eq '^replicaCount: *3' "$WS/lesson-2.3/prod-values.yaml" 2>/dev/null || exit 1 eq "$(j -n charts deploy shop-webapp '{.status.availableReplicas}')" 3 && eq "$(j -n charts svc shop-webapp '{.spec.type}')" NodePort || exit 1 [ "$(helm -n charts history shop -o json 2>/dev/null | python3 -c 'import sys,json; print(max(r["revision"] for r in json.load(sys.stdin)))')" -ge 2 ]""", hint=r"""A values file has the shape of `values.yaml`: `service:` on one line, `type: NodePort` indented under it. Then `helm upgrade shop CHART -n charts -f FILE`.""", show=r"""cat > {WS}/lesson-2.3/prod-values.yaml <<'EOF' replicaCount: 3 service: type: NodePort EOF helm -n charts upgrade shop {WS}/lesson-2.3/chart -f {WS}/lesson-2.3/prod-values.yaml kubectl -n charts rollout status deploy shop-webapp helm -n charts history shop"""), step("A new chart version, with your values", r"""Upgrade `shop` to chart version 0.2.0, `{WS}/lesson-2.3/webapp-0.2.0.tgz`, **keeping the values you set**. Careful: `helm upgrade` without values starts from the chart's defaults again, so your 3 replicas and the NodePort would be gone. `--reuse-values` keeps them (or pass `-f prod-values.yaml` once more).""", check=r""" helm -n charts list -o json 2>/dev/null | python3 -c 'import sys,json; r=[x for x in json.load(sys.stdin) if x["name"]=="shop"]; sys.exit(0 if r and r[0]["chart"]=="webapp-0.2.0" else 1)' || exit 1 eq "$(j -n charts deploy shop-webapp '{.spec.replicas}')" 3 && eq "$(j -n charts svc shop-webapp '{.spec.type}')" NodePort""", hint=r"""`helm upgrade shop {WS}/lesson-2.3/webapp-0.2.0.tgz -n charts --reuse-values`. `helm list -n charts` shows the chart version, `helm get values shop -n charts` the values in use.""", show=r"""helm -n charts upgrade shop {WS}/lesson-2.3/webapp-0.2.0.tgz --reuse-values helm -n charts list helm -n charts get values shop"""), step("Roll back", r"""Go back to revision **2**: chart 0.1.0 with your production values. Look at `helm history` before and after - a rollback is a new revision too, it does not rewind the counter.""", check=r""" helm -n charts history shop -o json 2>/dev/null | python3 -c ' import sys, json h = json.load(sys.stdin); last = max(h, key=lambda r: r["revision"]) sys.exit(0 if last["revision"] >= 4 and last["chart"] == "webapp-0.1.0" and last["status"] == "deployed" else 1)' || exit 1 eq "$(j -n charts deploy shop-webapp '{.spec.replicas}')" 3""", hint=r"""`helm history shop -n charts`, then `helm rollback shop 2 -n charts`.""", show=r"""helm -n charts history shop helm -n charts rollback shop 2 helm -n charts history shop"""), step("Clean up", r"""Uninstall release `shop`. Helm deletes everything the release created - the Deployment and the Service - and its history too, unless you add `--keep-history`. The Namespace stays.""", check=r"""! helm -n charts status shop >/dev/null 2>&1 && [ -z "$(j -n charts deploy shop-webapp '{.metadata.name}')" ] && [ -z "$(j -n charts svc shop-webapp '{.metadata.name}')" ]""", hint=r"""`helm uninstall shop -n charts`. `helm list -a -n charts` would still show a release uninstalled with `--keep-history`.""", show=r"""helm -n charts uninstall shop kubectl -n charts get all"""), ], "outro": r"""You rendered a chart, installed it, upgraded it with a values file and to a new chart version, rolled it back, and removed it. **For the exam** - `helm repo add NAME URL`, `helm repo update` and `helm search repo` find charts; a directory or a `.tgz` works wherever a chart name does. - `helm show values` and `helm template` let you look before you install; `helm get values RELEASE` shows what a release runs with. - `--set` for one value, `-f values.yaml` for several. On `upgrade`, values you do not pass go back to the chart's defaults unless you add `--reuse-values`. - `helm history` and `helm rollback RELEASE REVISION`: every change, a rollback included, is a new revision. - Always give the Namespace: `-n NAMESPACE`, and `--create-namespace` on install. Next, prove it against the clock: **drill 2.3**.""", } # ---- 2.4 Kustomize ------------------------------------------------------------------------------ _RENDER = r""" # rendered NAME: the overlay's objects as JSON, one per line, checked without applying anything rendered() { $K kustomize "$WS/lesson-2.4/overlays/$1" 2>/dev/null | $K create --dry-run=client -f - -o json 2>/dev/null | python3 -c ' import sys, json t, dec, i = sys.stdin.read(), json.JSONDecoder(), 0 while True: # one List, or one JSON document per object - kubectl has printed both while i < len(t) and t[i].isspace(): i += 1 if i >= len(t): break o, i = dec.raw_decode(t, i) for x in (o.get("items", []) if o.get("kind") == "List" else [o]): print(json.dumps(x))' } """ L24 = { "id": 9204, "objective": "2.4", "workers": 0, "title": "Kustomize: one base, two environments", "intro": r"""Kustomize builds plain YAML from a directory that holds a `kustomization.yaml`. There are no templates and no variables: a **base** holds the manifests, and an **overlay** points at the base and changes it - Namespace, names, labels, images, replicas, or anything else through a patch - without editing it. ``` base/ web: Deployment + Service overlays/staging/ staging-web in staging overlays/prod/ prod-web in prod, 3 x 1.27 ``` `kubectl` has Kustomize built in: `kubectl kustomize DIR` renders, `kubectl apply -k DIR` applies. In `{WS}/lesson-2.4/base` you will find a Deployment `web` (`nginx:1.25`, 1 replica), a Service `web` and their `kustomization.yaml`. The Namespaces `staging` and `prod` exist already.""", "setup": NS + r""" mkns staging; mkns prod d="$WS/lesson-2.4"; rm -rf "$d"; mkdir -p "$d/base" cat > "$d/base/deployment.yaml" <<'EOF' apiVersion: apps/v1 kind: Deployment metadata: name: web labels: {app: web} spec: replicas: 1 selector: matchLabels: {app: web} template: metadata: labels: {app: web} spec: containers: - name: web image: nginx:1.25 ports: - containerPort: 80 EOF cat > "$d/base/service.yaml" <<'EOF' apiVersion: v1 kind: Service metadata: name: web spec: selector: {app: web} ports: - port: 80 targetPort: 80 EOF cat > "$d/base/kustomization.yaml" <<'EOF' resources: - deployment.yaml - service.yaml EOF ( cd "$d/base" && sha256sum deployment.yaml service.yaml kustomization.yaml ) > "$PRIV/lesson-2.4-base.sum" """, "steps": [ step("Render the base", r"""See what Kustomize makes of the base before you change anything: render `{WS}/lesson-2.4/base` into `{WS}/lesson-2.4/base.yaml`, and read it.""", check=r"""f="$WS/lesson-2.4/base.yaml"; grep -q 'kind: Deployment' "$f" 2>/dev/null && grep -q 'kind: Service' "$f" && grep -q 'image: nginx:1.25' "$f" """, hint=r"""`kubectl kustomize DIR` prints the result - the same as `kustomize build DIR`. Redirect it with `> FILE`.""", show=r"""kubectl kustomize {WS}/lesson-2.4/base > {WS}/lesson-2.4/base.yaml cat {WS}/lesson-2.4/base.yaml"""), step("An overlay for staging", r"""Create `{WS}/lesson-2.4/overlays/staging/kustomization.yaml`. It uses the base `../../base`, puts everything into Namespace `staging`, prefixes every name with `staging-`, and gives every resource the label `env: staging`. For the label, use `labels:` with `includeSelectors: true`, which adds it to the selectors too. The older `commonLabels` does the same but is deprecated. Render the overlay to check it.""", check=_RENDER + r""" rendered staging | python3 -c ' import sys, json items = [json.loads(line) for line in sys.stdin if line.strip()] by = {(o["kind"], o["metadata"]["name"]): o for o in items} dep, svc = by.get(("Deployment", "staging-web")), by.get(("Service", "staging-web")) ok = bool(dep and svc) and all(o["metadata"].get("namespace") == "staging" and o["metadata"].get("labels", {}).get("env") == "staging" for o in (dep, svc)) sys.exit(0 if ok and dep["spec"]["template"]["metadata"]["labels"].get("env") == "staging" else 1)'""", hint=r"""``` resources: - ../../base namespace: staging namePrefix: staging- labels: - pairs: env: staging includeSelectors: true ```""", show=r"""mkdir -p {WS}/lesson-2.4/overlays/staging cat > {WS}/lesson-2.4/overlays/staging/kustomization.yaml <<'EOF' resources: - ../../base namespace: staging namePrefix: staging- labels: - pairs: env: staging includeSelectors: true EOF kubectl kustomize {WS}/lesson-2.4/overlays/staging"""), step("Apply staging", r"""Apply the staging overlay with kubectl's built-in Kustomize, then look at what you got.""", check=r""" eq "$(j -n staging deploy staging-web '{.status.availableReplicas}')" 1 && eq "$(j -n staging deploy staging-web '{.metadata.labels.env}')" staging \ && [ -n "$(j -n staging svc staging-web '{.metadata.name}')" ]""", hint=r"""`kubectl apply -k DIR` - `-k` instead of `-f`.""", show=r"""kubectl apply -k {WS}/lesson-2.4/overlays/staging kubectl -n staging get deploy,svc --show-labels"""), step("A prod overlay: replicas and image", r"""Production differs in more than its name. Create the overlay `{WS}/lesson-2.4/overlays/prod`: the same base, Namespace `prod`, prefix `prod-`, label `env: prod`, **3** replicas and image `nginx:1.27`. Use Kustomize's own fields for the last two, `replicas:` and `images:` - do not copy the Deployment. Render it to check.""", check=_RENDER + r""" rendered prod | python3 -c ' import sys, json items = [json.loads(line) for line in sys.stdin if line.strip()] by = {(o["kind"], o["metadata"]["name"]): o for o in items} dep = by.get(("Deployment", "prod-web")) ok = bool(dep) and dep["metadata"].get("namespace") == "prod" and dep["metadata"].get("labels", {}).get("env") == "prod" ok = ok and dep["spec"].get("replicas") == 3 and dep["spec"]["template"]["spec"]["containers"][0]["image"] == "nginx:1.27" sys.exit(0 if ok and ("Service", "prod-web") in by else 1)'""", hint=r"""Besides `resources`, `namespace`, `namePrefix` and `labels`: ``` replicas: - name: web count: 3 images: - name: nginx newTag: "1.27" ``` `replicas` names the Deployment by its base name; `images` names the image, not the container.""", show=r"""mkdir -p {WS}/lesson-2.4/overlays/prod cat > {WS}/lesson-2.4/overlays/prod/kustomization.yaml <<'EOF' resources: - ../../base namespace: prod namePrefix: prod- labels: - pairs: env: prod includeSelectors: true replicas: - name: web count: 3 images: - name: nginx newTag: "1.27" EOF kubectl kustomize {WS}/lesson-2.4/overlays/prod"""), step("Patch what the fields cannot reach", r"""For anything else there is a patch. Give the `web` container in prod a memory limit of `128Mi` with a patch file in the prod overlay, listed under `patches:` - then apply the prod overlay.""", check=r""" eq "$(j -n prod deploy prod-web '{.status.availableReplicas}')" 3 \ && eq "$(j -n prod deploy prod-web '{.spec.template.spec.containers[0].image}')" nginx:1.27 \ && eq "$(j -n prod deploy prod-web '{.spec.template.spec.containers[0].resources.limits.memory}')" 128Mi \ && eq "$(j -n prod deploy prod-web '{.metadata.labels.env}')" prod""", hint=r"""A strategic-merge patch looks like a small Deployment that names only what changes - by the **base** name `web`: ``` apiVersion: apps/v1 kind: Deployment metadata: name: web spec: template: spec: containers: - name: web resources: limits: memory: 128Mi ``` and in `kustomization.yaml`: `patches:` with `- path: limits.yaml`.""", show=r"""cat > {WS}/lesson-2.4/overlays/prod/limits.yaml <<'EOF' apiVersion: apps/v1 kind: Deployment metadata: name: web spec: template: spec: containers: - name: web resources: limits: memory: 128Mi EOF cat >> {WS}/lesson-2.4/overlays/prod/kustomization.yaml <<'EOF' patches: - path: limits.yaml EOF kubectl apply -k {WS}/lesson-2.4/overlays/prod kubectl -n prod rollout status deploy prod-web"""), step("One base, two environments", r"""Look at both environments side by side: `kubectl get deploy -A -l app=web -o wide`. They come from one base that you never edited - the exam checks that too. Write the image that **staging** runs to `{WS}/lesson-2.4/staging-image.txt`.""", check=r""" ( cd "$WS/lesson-2.4/base" && sha256sum -c --quiet "$PRIV/lesson-2.4-base.sum" >/dev/null 2>&1 ) || exit 1 eq "$(tr -d ' \r\n' < "$WS/lesson-2.4/staging-image.txt" 2>/dev/null)" nginx:1.25 || exit 1 [ -n "$(j -n staging deploy staging-web '{.metadata.name}')" ] && [ -n "$(j -n prod deploy prod-web '{.metadata.name}')" ]""", hint=r"""`-o jsonpath='{.spec.template.spec.containers[0].image}'` prints just the image.""", show=r"""kubectl get deploy -A -l app=web -o wide kubectl -n staging get deploy staging-web -o jsonpath='{.spec.template.spec.containers[0].image}' > {WS}/lesson-2.4/staging-image.txt cat {WS}/lesson-2.4/staging-image.txt"""), ], "outro": r"""You rendered a base, built two overlays on it with Kustomize's own fields and a patch, and applied both - and the base stayed as it was. **For the exam** - `kubectl kustomize DIR` renders, `kubectl apply -k DIR` applies, `kubectl delete -k DIR` removes. - An overlay lists its base under `resources:` and never edits it. - Built-in fields first - `namespace`, `namePrefix` / `nameSuffix`, `labels`, `images`, `replicas` - and a patch for anything else. A patch names objects by their **base** names. - `configMapGenerator` and `secretGenerator` make ConfigMaps and Secrets whose names change with their content, so a Deployment that uses one rolls when the data changes - drill 2.4 has one. Next, prove it against the clock: **drill 2.4**.""", } LESSONS = [L21, L22, L23, L24]