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.