Session 05 demos – Kubernetes: architecture, kubectl, Pods
Demos 1–3 belong to Block A (cluster architecture, the API, the first Pod), Demos 4–5 to Block B (Pod lifecycle, statuses, multi-container Pods). Files next to this one:
| File | Used in |
|---|---|
namespace.yaml |
Demo 3 — the demo namespace |
pod.yaml |
Demos 3–4 — pinboard-api as a single Pod |
pod-bad-image.yaml |
Demo 4 — ImagePullBackOff on purpose |
pod-sidecar.yaml |
Demo 5 — init container + native sidecar + emptyDir |
Before the session
~10 minutes, with network. Create the cluster before the room fills up — the
first kind create cluster pulls a ~1 GB node image.
cd ~/docker-kubernetes-training/demos/session05
kind get clusters # reuse "pinboard" if it is already there
kind create cluster --name pinboard --config ../../labs/kind-config.yaml
# The image students built in Lab 02, side-loaded into every node:
docker build -t pinboard-api:1.0 --build-arg APP_VERSION=1.0 ../../src/pinboard-api
kind load docker-image pinboard-api:1.0 --name pinboard
docker exec pinboard-worker crictl pull docker.io/library/busybox:1.37
kubectl config use-context kind-pinboard
kubectl get nodes
- Terminal: two panes — left for commands, right parked on
kubectl -n demo get pods -w. kubectlmust be ≥ 1.32 (kubectl version); the sidecar demo needs a 1.29+ API server, and the course baseline is 1.33.- Delete the
demonamespace if a previous run left it:kubectl delete ns demo --ignore-not-found. - If you are sharing the cluster with the class, say once: “everything I do lives
in the
demonamespace; your lab lives inpinboard.”
If the demo cluster is broken and you must rebuild it live, do it while talking through the architecture slide —
kind create clustertakes 60–90 s with the node image already pulled.
Demo 1: the cluster is containers all the way down (≈ 7 min)
What to show / say
kind is not a simulator: it is a real Kubernetes cluster whose nodes are Docker containers — which makes the architecture slide inspectable.
Steps
-
Create the cluster (or show that it exists) and read the config aloud:
cat ../../labs/kind-config.yaml kind get clusters kubectl cluster-info --context kind-pinboardKubernetes control plane is running at https://127.0.0.1:39421 CoreDNS is running at https://127.0.0.1:39421/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy👉 point at
extraPortMappings80/443 — “that is how Ingress will be reachable athttp://localhostin Session 07” — and at the tworole: workerentries. -
The nodes:
kubectl get nodes -o wideNAME STATUS ROLES AGE VERSION INTERNAL-IP OS-IMAGE CONTAINER-RUNTIME pinboard-control-plane Ready control-plane 9m v1.33.1 172.18.0.4 Debian GNU/Linux 12 containerd://2.0.x pinboard-worker Ready <none> 9m v1.33.1 172.18.0.2 Debian GNU/Linux 12 containerd://2.0.x pinboard-worker2 Ready <none> 9m v1.33.1 172.18.0.3 Debian GNU/Linux 12 containerd://2.0.x👉 point at
CONTAINER-RUNTIME: containerd— “not Docker. Kubernetes talks CRI to containerd, which talks OCI to runc. Docker as a runtime was removed in 1.24; the images we build with Docker still run here because they are OCI images.” -
Now the punchline:
docker ps --format 'table \t\t'NAMES IMAGE STATUS pinboard-control-plane kindest/node:v1.33.1 Up 9 minutes pinboard-worker kindest/node:v1.33.1 Up 9 minutes pinboard-worker2 kindest/node:v1.33.1 Up 9 minutes👉 “Three ‘machines’ — three containers on my laptop. Kubernetes IN Docker. On AKS/EKS/GKE these are VMs; nothing else changes.”
-
The control plane is itself Pods:
kubectl get pods -A -o wideNAMESPACE NAME READY STATUS NODE kube-system coredns-668d6bf9bc-7qk2m 1/1 Running pinboard-control-plane kube-system etcd-pinboard-control-plane 1/1 Running pinboard-control-plane kube-system kindnet-2xk9f 1/1 Running pinboard-worker kube-system kube-apiserver-pinboard-control-plane 1/1 Running pinboard-control-plane kube-system kube-controller-manager-pinboard-control-plane 1/1 Running pinboard-control-plane kube-system kube-proxy-8dhs4 1/1 Running pinboard-worker kube-system kube-scheduler-pinboard-control-plane 1/1 Running pinboard-control-plane local-path-storage local-path-provisioner-… 1/1 Running pinboard-control-plane👉 walk the slide’s boxes against this list: apiserver (the only thing that talks to etcd), etcd, scheduler, controller-manager — all on the control-plane node; kube-proxy and kindnet (the CNI) on every node, which is why there are several copies. “
local-path-provisioneris kind’s default StorageClass — Session 08. And note: it is Pods watching Pods. There is no magic layer underneath.” -
One level deeper, for the infra people in the room (optional, 20 s):
docker exec pinboard-control-plane crictl ps --name kube-apiserver👉 “
crictlis the CRI-leveldocker ps. When kubelet cannot start a container, this is where the truth is.”
Demo 2: the API, explain, and how to find anything (≈ 5 min)
What to show / say
Kubernetes is an API with a typed schema. You never need to guess a field name — the cluster tells you.
Steps
-
What this cluster can do:
kubectl api-resources | head -15 kubectl api-resources | wc -lNAME SHORTNAMES APIVERSION NAMESPACED KIND bindings v1 true Binding configmaps cm v1 true ConfigMap endpoints ep v1 true Endpoints events ev v1 true Event namespaces ns v1 false Namespace nodes no v1 false Node persistentvolumeclaims pvc v1 true PersistentVolumeClaim pods po v1 true Pod … 62👉 point at the SHORTNAMES column (“
po,ns,svc,deploy— that is where they come from”) and at NAMESPACED (“Nodes and Namespaces are cluster-scoped;-ndoes nothing for them”). “Sixty-two kinds, and you will use ten.” -
The schema, live:
kubectl explain pod kubectl explain pod.spec.containers.resourcesKIND: Pod VERSION: v1 FIELDS: apiVersion <string> kind <string> metadata <ObjectMeta> spec <PodSpec> status <PodStatus>👉 “apiVersion, kind, metadata, spec, status — every object in Kubernetes has these five.
specis what you want,statusis what the cluster observes. The whole system is a loop that tries to make the second match the first.” -
The whole tree when you are lost:
kubectl explain pod.spec --recursive | head -30👉 “Every field of a PodSpec, from this cluster’s own OpenAPI schema — so it is never out of date with the docs of a different version. Pipe it to
grepwhen you half-remember a field name:kubectl explain pod.spec --recursive | grep -i grace.”
Demo 3: the first Pod (≈ 8 min)
What to show / say
Imperative for exploring, declarative for keeping. Then the four commands you
will use every day: get, describe, logs, port-forward.
Steps
-
Imperative first — one line, no file:
kubectl apply -f namespace.yaml kubectl -n demo run scratch --image=busybox:1.37 --restart=Never -- sleep 300 kubectl -n demo get pods👉 “Fast to type, impossible to review, impossible to repeat. Fine for a debug Pod, never for anything that matters. Now the same thing as a file.”
-
The manifest — read it on screen before applying:
cat pod.yaml kubectl -n demo apply -f pod.yaml kubectl -n demo get pods -o wideNAME READY STATUS RESTARTS AGE IP NODE pinboard-api 1/1 Running 0 8s 10.244.1.4 pinboard-worker scratch 1/1 Running 0 40s 10.244.2.3 pinboard-worker2👉 point at the IP (“every Pod gets its own — that is the Kubernetes network model, Session 07”) and at the NODE (“I never said where. The scheduler picked it, using the
requestsin the file”). Also:imagePullPolicy: IfNotPresentis what makeskind load docker-imagework — withAlwaysit would try Docker Hub and fail. -
describe— the single most useful command in Kubernetes:kubectl -n demo describe pod pinboard-api | tail -25QoS Class: Burstable Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 35s default-scheduler Successfully assigned demo/pinboard-api to pinboard-worker Normal Pulled 34s kubelet Container image "pinboard-api:1.0" already present on machine Normal Created 34s kubelet Created container: api Normal Started 34s kubelet Started container api👉 read the event list as a story: scheduled → image → created → started. “When a Pod misbehaves, this bottom section answers why nine times out of ten.” Point at
QoS Class: Burstable— “requests set, limits not equal to requests. Session 09.” -
Logs — the same stdout we watched with
docker logs:kubectl -n demo logs pinboard-api | tail -3{"time":"…","level":"WARN","msg":"DATABASE_URL not set — using in-memory store, notes will be lost on restart"} {"time":"…","level":"INFO","msg":"pinboard-api starting","version":"1.0","theme":"emerald","store":"memory","addr":":8080"}👉 “JSON, because the manifest sets
LOG_FORMAT=json. Same container, same process, same stdout — only the collector changed.” -
Reach it without a Service (there is none yet):
kubectl -n demo port-forward pod/pinboard-api 8080:8080 & sleep 2 && curl -s localhost:8080/api/info | python3 -m json.tool kill %1{ "app": "pinboard-api", "greeting": "Hello from a Pod", "hostname": "pinboard-api", "store": "memory", "version": "1.0" }👉 point at
"hostname": "pinboard-api"— “the Pod name is the hostname.” And: “port-forwardis a tunnel through the API server to one Pod, for debugging only. It is not how users reach an app — that is Session 07.” -
A shell in a Pod that has no shell:
kubectl -n demo exec -it pinboard-api -- sherror: Internal error occurred: failed to exec in container: … exec: "sh": executable file not found in $PATH👉 “Our distroless image has no shell — the thing we celebrated in Session 02 is now inconvenient.
kubectl debug -it pinboard-api --image=busybox --target=apiattaches a second container with a shell into the same namespaces. That is Session 09; just plant the flag.”
Demo 4: the status zoo — events and ImagePullBackOff (≈ 7 min)
What to show / say
Pods do not fail silently. Watch the events stream, then break one on purpose and read its status like a report.
Steps
-
Start watching events in the right pane:
kubectl -n demo get events --watch -
In the left pane, apply the broken Pod:
kubectl -n demo apply -f pod-bad-image.yaml kubectl -n demo get pods -wNAME READY STATUS RESTARTS AGE pinboard-api-bad 0/1 Pending 0 0s pinboard-api-bad 0/1 ContainerCreating 0 1s pinboard-api-bad 0/1 ErrImagePull 0 4s pinboard-api-bad 0/1 ImagePullBackOff 0 18sCtrl-C the watch. 👉 walk the states: “Pending = the scheduler has not placed it (or cannot). ContainerCreating = kubelet is pulling and mounting. ErrImagePull = the pull failed once. ImagePullBackOff = it is retrying with a growing delay. Note that Kubernetes never gives up — unlike
--restart on-failure:3in Docker.” -
The events pane, meanwhile:
0s Warning Failed pod/pinboard-api-bad Failed to pull image "pinboard-api:9.9-does-not-exist": failed to resolve reference … 0s Warning Failed pod/pinboard-api-bad Error: ErrImagePull 0s Normal BackOff pod/pinboard-api-bad Back-off pulling image "pinboard-api:9.9-does-not-exist"👉 “
dockertold us this on the terminal. Kubernetes is asynchronous — nobody is watching your terminal, so it writes events instead. Events expire after an hour; that is whydescribeis your first stop, not your last.” -
Read the machine-readable version — the
statusblock from the slide:kubectl -n demo get pod pinboard-api-bad -o yaml | sed -n '/^status:/,$p' | head -25status: conditions: - lastTransitionTime: "…" status: "True" type: PodScheduled - status: "False" type: Ready reason: ContainersNotReady containerStatuses: - image: pinboard-api:9.9-does-not-exist name: api ready: false restartCount: 0 started: false state: waiting: message: 'Back-off pulling image "pinboard-api:9.9-does-not-exist"' reason: ImagePullBackOff phase: Pending👉 “This is the half of the object you never write.
specis your intent,statusis the cluster reporting back — including exactly why it cannot comply. Every controller in the next four sessions works by reading a status like this and acting on it.” -
Targeted queries instead of eyeballing YAML:
kubectl -n demo get pod pinboard-api-bad \ -o jsonpath='{.status.containerStatuses[0].state.waiting.reason}{"\n"}' kubectl -n demo get events --sort-by=.lastTimestamp | tail -5👉 “
-o jsonpathfor scripts,--sort-by=.lastTimestampbecause the default event order is not chronological. Both come back in Session 09’s troubleshooting method.”
Demo 5: a Pod with more than one container (≈ 6 min)
What to show / say
A Pod is not “a container”. It is a group that shares a network namespace and volumes — which is what init containers and sidecars are built on.
Steps
-
Read the file, pointing at the three containers before applying:
kubectl -n demo apply -f pod-sidecar.yaml kubectl -n demo get pod pinboard-api-sidecar -wNAME READY STATUS RESTARTS AGE pinboard-api-sidecar 0/2 Init:0/2 0 1s pinboard-api-sidecar 0/2 Init:1/2 0 3s pinboard-api-sidecar 1/2 Running 0 5s pinboard-api-sidecar 2/2 Running 0 7s👉
READY 2/2, not1/1: a restartable init container (the sidecar) counts as a container of the Pod, whileinit-marker, which runs to completion, does not.👉 “
Init:0/2→Init:1/2: the first init container ran and exited, and only then did the next one start. Ordered, sequential, before the app.” -
Who is in there:
kubectl -n demo get pod pinboard-api-sidecar \ -o jsonpath='{range .spec.initContainers[*]}init: {.name} restartPolicy={.restartPolicy}{"\n"}{end}{range .spec.containers[*]}app: {.name}{"\n"}{end}'init: init-marker restartPolicy= init: poller restartPolicy=Always app: api👉 the one line that matters: “
polleris declared underinitContainers, but withrestartPolicy: Always. That makes it a native sidecar: it starts before the app containers, it keeps running next to them, it is restarted if it dies, and it is shut down only after the app container. Before Kubernetes 1.29 you had to fake this with an ordinary container and it broke Jobs, because the Pod never finished.” -
Proof of the shared network namespace:
kubectl -n demo logs pinboard-api-sidecar -c poller --tail 3api not answering yet {"app":"pinboard-api","greeting":"Hello from a multi-container Pod","hostname":"pinboard-api-sidecar","store":"memory","theme":"emerald","uptime":"12s","version":"1.0"}👉 “The sidecar called 127.0.0.1:8080 and reached the API container. Two containers, one network namespace, one IP — exactly the
/proc/<pid>/nspicture from Session 01, only now Kubernetes builds the group for us. Also note the first line: the sidecar was up before the API. That is expected, and it is why sidecars must tolerate the app not being there yet.” -
Proof of the shared volume:
kubectl -n demo exec pinboard-api-sidecar -c poller -- head -2 /shared/probe.logpod started at 2026-03-04T09:12:31Z {"app":"pinboard-api",…}👉 “Line one was written by
init-marker, which exited before the sidecar started. TheemptyDirvolume is the Pod’s — it outlives individual containers and dies with the Pod. That is the log-shipper pattern, the config-renderer pattern and the service-mesh proxy pattern, all in eight lines of YAML.” -
Wrap up into the lab:
kubectl -n demo get pods👉 “In the lab you will do this in the
pinboardnamespace with your own image — and you will notice that deleting a Pod is final. Nothing recreates it. Session 06 is about the controller that does.”
Reset
Lab 05 expects: the pinboard cluster running, pinboard-api:1.0 loaded into
it, and no pinboard namespace (students create it).
cd ~/docker-kubernetes-training/demos/session05
kubectl delete -f pod-sidecar.yaml -f pod-bad-image.yaml -f pod.yaml --ignore-not-found
kubectl -n demo delete pod scratch --ignore-not-found
kubectl delete namespace demo --ignore-not-found # removes everything above at once
kubectl delete namespace pinboard --ignore-not-found # in case an earlier run created it
Verify:
kubectl get nodes # 3 Ready
kubectl get ns # no demo, no pinboard
docker exec pinboard-control-plane crictl images | grep pinboard-api # 1.0 still loaded
Only if the cluster itself is broken:
kind delete cluster --name pinboardand re-run the Before the session block. Never do this between the demo and the lab without warning the room — every student’s kubeconfig context is invalidated.