10.22 - Challenges

~25 graded challenges. Each has: task, hint, verify (the thing that must be true when you are done). Do them with manifests in a scratch namespace:

kubectl create namespace lab
kubectl config set-context --current --namespace=lab

Reset anytime with kubectl delete namespace lab (then recreate it). Never do challenges in the default namespace for long, and never touch kube-system except to look.

Level 1 - Get your hands on (Day 1)

C1 - First pod

Run a pod named hello running nginx:alpine, confirm it is Running, then delete it. Hint: kubectl run hello --image=nginx:alpine is fine here; imperative is OK for throwaway. Verify: kubectl get pod hello showed Running before the delete.

C2 - Port-forward

Start a deployment web (nginx:alpine), port-forward it to localhost:8080, and fetch a page from it. Hint: kubectl port-forward deployment/web 8080:80 in one terminal, curl -I http://localhost:8080 in another. Expect HTTP/1.1 200 OK. Verify: curl returns 200.

C3 - Exec and logs

In the web deployment pod, write a file to /tmp, print nginx’s log, and confirm you can read the file back. Hint: kubectl exec -it deployment/web -- bash then echo hi > /tmp/marker && cat /tmp/marker; exit, then kubectl logs deployment/web --tail=10. Verify: you saw nginx access logs containing your requests.

Level 2 - Workloads (Day 2)

C4 - Deployment from scratch

Write hello.yaml by hand: Deployment hello, 2 replicas, image nginxdemos/hello:plain-text (or nginx:alpine). No copy-paste from 10.32. Hint: the manifest needs apiVersion, kind, metadata.name, spec.replicas, spec.selector.matchLabels, spec.template.metadata.labels, spec.template.spec.containers[0].{name,image}. Verify: kubectl get deploy hello shows 2/2 READY.

C5 - Rolling update and rollback

Update hello.yaml to a different image tag, kubectl apply, watch the rollout; then kubectl rollout undo deployment/hello. Hint: kubectl rollout status deployment/hello waits for the update; kubectl rollout history deployment/hello shows revisions; undo goes back one. Verify: after undo, kubectl get deploy hello -o jsonpath='{.spec.template.spec.containers[0].image}' prints the ORIGINAL image.

C6 - Scale and self-heal

Scale hello to 5, kill one pod, and watch the Deployment bring it back without you doing anything. Hint: kubectl scale deployment hello --replicas=5; kubectl delete pod -l app=hello deletes one (the selector deletes all matching, so check the count first); kubectl get pods should show 5 again within seconds. Verify: 5/5 READY, and the replacement pod has a different name than the one you killed.

Level 3 - Networking (Day 3)

C7 - Service discovery

Deploy two apps: api (any http server, e.g. nginx) and web (another nginx), each with a ClusterIP Service. From inside the web pod, curl the api service BY NAME. Hint: create a Service manifest kind: Service, spec.selector: {app: api}; inside the web pod run curl http://api:80 (service name = DNS name, same namespace); use kubectl exec -it deploy/web -- sh -c "curl -s http://api | head -1". Verify: the curl from inside the cluster succeeds; curl api from your HOST does NOT (no route to host / cannot resolve).

C8 - NodePort

Convert the web Service to NodePort and hit it from the host. Hint: spec.type: NodePort, spec.ports[0].nodePort in 30000-32767, or let k8s assign one (kubectl get svc web shows it). Then curl http://localhost:<nodePort>. Verify: the host curl returns 200.

C9 - Ingress (after enabling Traefik per 10.41)

Create an Ingress routing lab.example to the web Service. Then point your host’s /etc/hosts at it and fetch a page. Hint: kind: Ingress, spec.rules[0].host: lab.example, http.paths[0].backend.service.name: web. Test with curl -H "Host: lab.example" http://localhost:<traefik-http-port>. Verify: you get a 200 with the Host header set (no DNS edit needed if you test with the Host header).

Level 4 - Config (Day 4)

C10 - ConfigMap as env

ConfigMap greeting with message=hello-from-configmap; Deployment envtest (busybox or alpine) that prints $MESSAGE to its log. Hint: spec.template.spec.containers[0].env[0].{name: MESSAGE, valueFrom.configMapKeyRef.{name: greeting, key: message}}; busybox: command: ["sh","-c","echo $MESSAGE; sleep 3600"]; check kubectl logs deploy/envtest. Verify: the log contains hello-from-configmap.

C11 - ConfigMap as file

Same ConfigMap, but mount it at /etc/config in the pod; confirm the key appears as a file. Hint: spec.template.spec.containers[0].volumeMounts[0].{name: cfg, mountPath: /etc/config} + spec.template.spec.volumes[0].{name: cfg, configMap: {name: greeting}}. Check kubectl exec deploy/envtest -- ls /etc/config and cat /etc/config/message. Verify: the file exists and contains the value.

C12 - Secret

Create Secret dbpass with password=supersecret; mount it as env var DB_PASSWORD. Confirm the value shows in the pod, then look at how the Secret is stored with kubectl get secret dbpass -o yaml. Hint: kubectl create secret generic dbpass --from-literal=password=supersecret; note base64 in the yaml. kubectl get secret dbpass -o jsonpath='{.data.password}' then decode with base64 -d. Verify: you saw base64 in -o yaml and the decoded value matches. Know the answer to: “is a Secret encrypted at rest in etcd by default?” (No, not without encryption config; base64 is encoding.)

Level 5 - Storage (Day 5)

C13 - PVC that survives pod death

PVC mydata (1Gi, default storage class) + Deployment writer mounting it at /data, writing a file; delete the pod; confirm the file is still there via the replacement pod. Hint: PVC kind: PersistentVolumeClaim, spec.accessModes: [ReadWriteOnce], resources.requests.storage: 1Gi; deployment mounts claimName mydata. kubectl exec deploy/writer -- sh -c "echo survive > /data/marker", delete pod, exec again and cat. Verify: /data/marker still exists after pod replacement.

C14 - Per-pod storage with a StatefulSet

3-replica StatefulSet where pod 0 has a file that pod 1 does NOT see. Hint: kind: StatefulSet with spec.serviceName set (headless service required, create one), volumeClaimTemplates (NOT a plain volume) with storageClassName local-path. Write /data/mine in db-0 and try to read it in db-1. Verify: names are db-0, db-1, db-2; kubectl get pvc shows one PVC PER POD (data-db-0, data-db-1, …); the file is visible only in db-0.

C15 - StorageClass inspection

Explain what the local-path StorageClass does and why it is WaitForFirstConsumer. Hint: kubectl get sc, kubectl get sc local-path -o yaml. It’s k3s’s default provisioner; it creates hostPath-style dirs under /var/lib/rancher/ k3s/storage on the node the pod lands on. WaitForFirstConsumer means the PV is provisioned only after a pod needs it, so it lands on the pod’s node. Verify: you can name the provisioner and the default dir. (No command.)

Level 6 - Other controllers, limits, HPA (Day 6)

C16 - DaemonSet

DaemonSet nodeinfo (nginx) that puts one pod on every node. Hint: kind: DaemonSet, same shape as Deployment minus replicas. On a single-node cluster expect exactly 1 pod. kubectl get ds, kubectl get pods -o wide | grep nodeinfo. Verify: number of DaemonSet pods == number of Ready nodes.

C17 - Job and CronJob

A Job that sleeps 5s then completes; a CronJob that runs every minute and finishes; watch both, then delete the CronJob. Hint: kind: Job, spec.template.spec.restartPolicy: Never, containers[0]. command: ["sh","-c","sleep 5"]; CronJob adds spec.schedule: "* * * * *". kubectl get jobs; kubectl get cronjobs; wait ~70s for the first CronJob run; kubectl get pods -l job-name=.... Verify: Job shows COMPLETE; CronJob shows 1 successful schedule after a minute.

C18 - Resource limits that matter

Deployment with resources.requests.cpu: 100m, memory: 128Mi and resources.limits.cpu: 500m, memory: 256Mi; then confirm the node’s allocatable accounting. Hint: kubectl describe node shows “Allocated resources”; requests are what the scheduler reserves, limits are the hard cap (cgroup). A pod exceeding memory limit gets OOM-killed. Verify: the node describes your request amounts; you can explain request vs limit.

C19 - HPA autoscaling

Enable autoscaling on a deployment (kubectl autoscale deployment hello --cpu-percent=50 --min=1 --max=5 or an HPA manifest), generate CPU load inside a pod, watch replicas increase, then delete the HPA. Hint: load with kubectl exec deploy/hello -- sh -c "yes > /dev/null &", or use ab -n 100000 against the service; HPA needs metrics-server (k3s ships it) and a container with a CPU request (an HPA with no request has no target). kubectl get hpa -w. Verify: kubectl get hpa shows the target crossed and replicas above the min.

Level 7 - Debugging (Day 7)

C20 - Crashloop root cause

Apply a deployment whose container exits immediately (e.g. image busybox with command ["sh","-c","exit 1"]). Find the root cause using only describe, events, and logs. Then fix it. Hint: kubectl get events --sort-by=.lastTimestamp | tail shows BackOff/CrashLoopBackOff; kubectl logs <pod> --previous shows the exit; kubectl describe pod <name> shows the restart count and the last state. Fix = make the container not exit (or a Job with restartPolicy Never). Verify: you can state the exact reason (exit code 1, CrashLoopBackOff) from events/logs, and the fixed pod is Running.

C21 - ImagePullBackOff

Deploy with a nonexistent image tag (nginx:nonexistenttag). Diagnose from describe. Hint: kubectl describe pod -> Events: Failed to pull image … manifest unknown. The pull policy matters: with imagePullPolicy: IfNotPresent and no local image it still tries to pull. Fix the tag. Verify: you identified ImagePullBackOff/ErrImagePull from the events.

C22 - Wrong selector

A Service whose selector matches no pods. Prove it with endpoints. Hint: kubectl get endpoints <svc> shows <none> when the selector matches nothing. Check the service selector vs pod labels with kubectl get pods --show-labels. Fix the selector. Verify: kubectl get endpoints shows the pod IP after the fix.

C23 - Probe failure

Deployment with a liveness probe hitting a port the container does not listen on. Watch the pod restart loop, then fix the probe. Hint: livenessProbe: {httpGet: {path: /, port: 9999}}; kubectl describe pod shows Liveness probe failed and the restart count climbing. Fix: point the probe at the real port. Verify: restart count stops climbing after the fix.

C24 - Name collision

Two Services with the same name in the same namespace (or a Deployment name that collides). What does the API server say? Why is DNS now ambiguous? Hint: names are unique per kind+namespace: kubectl apply a second Service with the same name -> already exists (if you use create) or it replaces (apply). curl http://svcname from a pod may hit either. Verify you understand: DNS name -> one Service; a Deployment and a Service MAY share a name (different kinds), two Services may not. Verify: you can explain the failure mode, not just run a command.

C25 - The full reset

Delete your lab namespace; confirm kube-system is untouched; recreate the namespace; confirm a fresh default behaves normally. Hint: kubectl delete namespace lab (takes ~30s, terminating), then kubectl get ns, kubectl get pods -n kube-system. Verify: lab gone, kube-system healthy, and you know why deleting a namespace deleted all its workloads (namespace = the scope, controllers can’t run outside it).

Score guide

  • Level 1-2 without hints: solid foundation. 3-4: you could run a real stateless service. 5-6: you understand state and scale. 7: you can be the person who debugs the cluster at 2am.

This site uses Just the Docs, a documentation theme for Jekyll.