Pods: Anatomie und Lifecycle
Die kleinste deploybare Einheit in Kubernetes.
Lernziele
Nach dieser Lektion kannst du erklären, warum ein Pod — nicht der Container — die kleinste deploybare Einheit in Kubernetes ist und was Container in einem Pod tatsächlich teilen. Du kennst Init Containers und das Sidecar Pattern samt ihrer Einsatzgrenzen, kannst die Lifecycle-Phasen von Pending bis Succeeded/Failed auseinanderhalten und weißt, wie die Restart Policies Always, OnFailure und Never das Verhalten bei Abstürzen steuern.
Das Problem: Manche Prozesse gehören zusammen
Container isolieren Prozesse — das ist ihr Zweck. Aber manchmal ist Isolation genau falsch: Ein Log-Shipper muss die Log-Dateien der App lesen. Ein TLS-Proxy muss auf localhost vor der App sitzen. Ein Setup-Schritt muss ins selbe Verzeichnis schreiben, aus dem die App später liest. Zwei getrennte Container auf womöglich zwei verschiedenen Nodes können das nicht — und ein fetter Container mit App, Proxy und Shipper zusammen wirft alle Vorteile von Containern weg (getrennte Images, getrennte Updates, getrennte Verantwortung).
Der Pod ist die Antwort: eine Gruppe von Containern, die garantiert gemeinsam auf demselben Node laufen und sich zwei Dinge teilen — den Netzwerk-Namespace (eine gemeinsame Pod-IP; Container erreichen sich über localhost und müssen sich Ports teilen) und auf Wunsch Volumes (gemeinsame Verzeichnisse, etwa ein emptyDir). Die Dateisysteme der Container bleiben getrennt, die Prozess-Isolation auch. Technisch hält ein Infrastruktur-Container („pause”) die gemeinsamen Namespaces am Leben — das ist die Sandbox, die das kubelet in Lektion 4 als ersten Schritt erstellt hat.
Der Normalfall bleibt trotzdem: ein Container pro Pod. Mehrere Container gehören nur dann zusammen, wenn sie zwingend gemeinsam skalieren und gemeinsam leben und sterben sollen.
Init Containers und Sidecars: die zwei Mehrcontainer-Muster
Init Containers laufen vor den App-Containern — nacheinander, jeder muss erfolgreich beendet sein, bevor der nächste startet. Erst wenn alle durch sind, starten die eigentlichen Container. Typische Jobs: auf eine Abhängigkeit warten („bis die Datenbank erreichbar ist”), Schema-Migrationen, Konfiguration in ein gemeinsames Volume schreiben, Berechtigungen setzen. Der Charme: Das Setup-Werkzeug (mit psql, curl und Co.) verschwindet nach getaner Arbeit — das App-Image bleibt schlank, die Angriffsfläche klein.
Das Sidecar Pattern ergänzt die App um einen dauerhaft mitlaufenden Helfer im selben Pod: Log-Shipper, Metrics-Exporter, Service-Mesh-Proxy (Envoy in Istio ist das prominenteste Beispiel — injiziert übrigens per Mutating Webhook, Lektion 2). Die App bleibt unverändert; die Plattform-Funktion reist im Nachbarcontainer mit. Klassisch hatten Sidecars zwei Schmerzpunkte: Bei Jobs verhinderte der nie endende Sidecar das Job-Ende, und die Startreihenfolge war ungeregelt. Dafür gibt es inzwischen native Sidecar-Container: ein Init Container mit restartPolicy: Always, der vor der App startet, sie überlebt und das Job-Ende nicht mehr blockiert.
Der ehrliche Trade-off bei Sidecars: Jeder davon kostet Ressourcen pro Pod — bei 500 Pods sind 50 MB Sidecar-Memory 25 GB Cluster-weit. Genau deshalb experimentiert das Ökosystem mit Sidecar-losen Ansätzen (etwa eBPF-basierten Meshes).
Lifecycle: was Pending wirklich bedeutet
Ein Pod durchläuft Phasen, und sie präzise zu kennen ist die halbe Troubleshooting-Miete:
- Pending: akzeptiert, aber noch nicht alle Container laufen. Dahinter stecken zwei sehr verschiedene Zustände: noch kein Node zugewiesen (Scheduling-Problem, Lektion 3) — oder Node zugewiesen, aber Image-Pull bzw. Init Containers laufen noch.
- Running: an einen Node gebunden, alle Container erstellt, mindestens einer läuft.
- Succeeded / Failed: alle Container beendet — alle mit Exit-Code 0, oder mindestens einer nicht. Endzustände für Batch-Workloads.
Wichtig: Die Phase ist grob. Was kubectl get pods in der STATUS-Spalte zeigt — CrashLoopBackOff, ImagePullBackOff, Init:0/1 — sind abgeleitete Container-Zustände, keine Phasen. CrashLoopBackOff heißt: Der Container stürzt wiederholt ab, und das kubelet wartet mit exponentiell wachsendem Abstand (bis 5 Minuten) auf den nächsten Neustartversuch. Es ist ein Symptom, keine Ursache — die steht in kubectl logs --previous.
Zum Lifecycle gehört auch das Ende: Beim Löschen schickt das kubelet SIGTERM, wartet die terminationGracePeriodSeconds (Default 30) ab und killt dann mit SIGKILL. Apps, die SIGTERM ignorieren, verlieren bei jedem Deployment laufende Requests.
Restart Policies: wer darf sterben?
Die restartPolicy gilt pro Pod (nicht pro Container) und kennt drei Werte:
- Always (Default): Das kubelet startet beendete Container immer neu — auch nach Exit 0. Richtig für Dauerläufer wie Webserver; deshalb erzwingen Deployments diesen Wert.
- OnFailure: Neustart nur bei Fehler-Exit. Richtig für Batch-Jobs, die nach Erfolg fertig sind, bei Fehlern aber Retry verdienen.
- Never: kein Neustart, der Exit-Status bleibt zur Inspektion stehen.
Zwei Feinheiten, die in Interviews den Unterschied machen: Die Policy wirkt node-lokal — das kubelet startet Container auf demselben Node neu, es verschiebt nie einen Pod auf einen anderen Node. Rescheduling nach Node-Ausfall leistet erst ein Controller (Deployment, Job), der einen neuen Pod erstellt. Und: restartPolicy und Liveness Probe (Lektion 4) arbeiten zusammen — die Probe entscheidet, dass gekillt wird, die Policy, ob neu gestartet wird.
Praxis: Anatomie zum Anfassen
kind create cluster --name podlab
# Pod mit Init Container + zwei Containern, die ein Volume und localhost teilen
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: anatomy
spec:
restartPolicy: Always
initContainers:
- name: setup
image: busybox:1.36
command: ["sh", "-c", "echo '<h1>Init war hier</h1>' > /work/index.html"]
volumeMounts:
- { name: webroot, mountPath: /work }
containers:
- name: web
image: nginx:1.27
volumeMounts:
- { name: webroot, mountPath: /usr/share/nginx/html }
- name: checker
image: busybox:1.36
command: ["sh", "-c", "while true; do wget -qO- http://localhost; sleep 10; done"]
volumes:
- { name: webroot, emptyDir: {} }
EOF
# Phasen beobachten: Init:0/1 → PodInitializing → Running
kubectl get pod anatomy --watch &
sleep 20; kill %1
# Der checker erreicht nginx über localhost — geteilter Netzwerk-Namespace
kubectl logs anatomy -c checker --tail=2
# Lifecycle-Details: beide Container, eine Pod-IP, Init abgeschlossen
kubectl get pod anatomy -o jsonpath='{.status.podIP}{"\n"}{.status.phase}{"\n"}'
kubectl describe pod anatomy | grep -A4 "Init Containers:"
Experimentiere danach: Lösche index.html per kubectl exec anatomy -c web -- rm /usr/share/nginx/html/index.html und sieh, dass der checker 403 bekommt — gemeinsames Volume, getrennte Prozesse. Aufräumen: kind delete cluster --name podlab.
Typische Stolperfallen
kubectl logs ohne -c bei Multi-Container-Pods: Logs, exec und Ressourcen gelten pro Container. Wer den Sidecar debuggt, während er die App-Logs liest, verliert Zeit — -c <name> immer explizit setzen.
Port-Kollisionen im Pod: Geteilter Netzwerk-Namespace heißt geteilte Ports. Zwei Container, die beide auf 8080 lauschen wollen, brechen sich gegenseitig — ein Klassiker beim Nachrüsten von Sidecars.
Pods als langlebige Objekte behandeln: Ein Pod wird nie „geheilt” auf einen anderen Node verschoben — er ist an seinen Node gebunden und wird bei Node-Ausfall ersetzt, nicht migriert. Alles im Container-Dateisystem und in emptyDir ist dann weg. Pods sind Vieh, nicht Haustier; Identität und Persistenz sind Aufgaben höherer Abstraktionen (Lektionen 7 und 8).
CrashLoopBackOff am falschen Ende debuggen: Der aktuelle Container ist frisch gestartet und sagt nichts. Die Ursache steht im vorherigen Versuch: kubectl logs <pod> --previous, plus kubectl describe für Exit-Code (137 = OOMKilled, Brücke zu Lektion 10).
Interview-Vorbereitung
Auf „Was ist ein Pod und warum nicht einfach Container?” antwortest du problem-first: Manche Prozesse brauchen gemeinsames Netzwerk und gemeinsamen Speicher bei getrennten Images — der Pod liefert genau das (eine IP, localhost untereinander, optionale shared Volumes, garantiert ein Node) und ist deshalb die kleinste Scheduling-Einheit. Dann die Muster: Init Containers für Setup-Schritte davor, Sidecars für dauerhafte Helfer daneben — mit dem Ressourcen-Trade-off pro Pod. Abschließen mit der Vieh-Analogie: Pods sind ersetzbar, Management kommt von Controllern.
Typische Follow-ups:
- „Was teilen sich Container im Pod — und was nicht?” — Netzwerk-Namespace (IP, Ports, localhost) und gemountete Volumes; nicht: Dateisystem, Prozess-Isolation, Ressourcen-Limits.
- „Init Container vs. Sidecar?” — Init: läuft sequenziell vor der App und endet. Sidecar: läuft dauerhaft daneben; nativ als Init Container mit
restartPolicy: Always. - „Was bedeutet CrashLoopBackOff?” — Wiederholte Abstürze mit wachsender Neustart-Wartezeit; Diagnose über
logs --previousund Exit-Code, nicht über Neustarten. - „Pod-Restart vs. Pod-Ersatz?” — restartPolicy wirkt node-lokal auf Container; einen neuen Pod (anderer Node möglich) erstellt nur ein Controller.
Zusammenfassung
Der Pod bündelt Container, die zusammengehören: eine gemeinsame IP, localhost-Kommunikation, geteilte Volumes, ein gemeinsamer Lebenszyklus auf einem Node. Init Containers erledigen Setup sequenziell vorab, Sidecars laufen dauerhaft mit — beide mit klaren Kosten. Der Lifecycle läuft von Pending über Running zu Succeeded/Failed, mit CrashLoopBackOff als häufigstem Zwischenruf, und die Restart Policies Always/OnFailure/Never regeln Neustarts strikt node-lokal. Ein einzelner Pod hat keine Selbstheilung über Node-Grenzen und keine Update-Strategie.
Genau dafür gibt es die nächste Abstraktionsebene: In der nächsten Lektion verwalten Deployments Pods als ersetzbare Replicas — mit Rolling Updates und Rollbacks.