Kubernetes-Grundlagen durch Deployment-Druck
Unter dem Druck eines echten Deployments sortieren sich die Kubernetes-Konzepte von selbst nach Wichtigkeit – vom Pod über Deployment und Service bis zum Ingress.
Um zwei Uhr nachts stirbt ein Container. Kein Angriff, kein Hardware-Defekt – der Node.js-Prozess darin ist über ein Speicherleck gestolpert, das Betriebssystem hat ihn beendet, und mit dem Prozess ist auch der Container weg. Ab diesem Moment entscheidet eine einzige Frage über den Rest der Nacht: Wer startet ihn neu, wenn niemand hinschaut? Auf einem klassisch betriebenen Server ist die Antwort immer eine Kette aus Menschen und Halbautomatik – ein Monitoring, das jemanden anruft, eine systemd-Unit mit Restart-Regel, ein Admin mit SSH-Zugang und wenig Schlaf. Die Konsequenz aus solchen Nächten ist der eigentliche Grund, warum es Kubernetes gibt: ein System, in dem der Neustart keine menschliche Reaktion ist, sondern der Normalbetrieb einer Maschine, die ununterbrochen Soll und Ist vergleicht.
Die Doku erklärt alles, nur nicht die Reihenfolge
Wer Kubernetes über die Konzept-Dokumentation lernen will, bekommt ein Vokabelheft: Pods, ReplicaSets, Deployments, Services, Endpoints, Ingress, ConfigMaps, Secrets, Volumes, Namespaces, DaemonSets, StatefulSets, Jobs, CronJobs. Alles korrekt beschrieben, alles gleichberechtigt nebeneinander. Genau das ist das Problem. Die Doku ist ein Nachschlagewerk, keine Lernreihenfolge – und ohne Reihenfolge fühlt sich Kubernetes an wie eine Fremdsprache, bei der man erst sprechen darf, wenn man das komplette Wörterbuch auswendig kann.
Mir ist das dieses Jahr in einem Projekt aufgefallen, in dem eine bestehende Node.js-Anwendung samt Datenbank in ein Cluster umziehen sollte – mit Termin, nicht irgendwann. Unter diesem Deployment-Druck passierte etwas Interessantes: Die Konzepte sortierten sich von selbst. Nicht nach der Gliederung der Dokumentation, sondern nach der Frage, ohne welches Objekt die Anwendung jetzt nicht läuft. Übrig blieb eine erstaunlich kurze Liste:
- Pod – das, was wirklich läuft
- Deployment – das, was Pods am Leben hält
- Service – eine stabile Adresse vor unstabilen Pods
- Ingress – die Tür nach draußen
- ConfigMap und Secret – Konfiguration raus aus dem Image
Alles andere – StatefulSets, DaemonSets, NetworkPolicies, die Feinheiten von RBAC – kam erst später dran, als die Anwendung schon lief und der Druck nachgelassen hatte. Die Beispiele, an denen ich diese Reihenfolge durchgespielt habe, liegen öffentlich im Repository introduction-kubernetes: eine Materialsammlung rund um eine kleine Todo-Anwendung aus Frontend, Backend und Datenbank, lokal mit Minikube nachvollziehbar.
Zielzustand statt Kommando-Choreografie
Die wichtigste Grundeinsicht steht in keinem YAML-Beispiel, sondern dahinter: Kubernetes ist deklarativ. Man sagt dem Cluster nicht, was er tun soll, sondern beschreibt, wie die Welt aussehen soll, wenn alles gut ist. Drei Kopien dieses Containers, erreichbar unter diesem Namen, mit dieser Konfiguration. Ein Controller-Loop vergleicht diesen Zielzustand pausenlos mit der Wirklichkeit und korrigiert die Differenz. Stirbt ein Pod, fehlt einer – also wird einer gestartet. Nicht weil jemand einen Befehl gibt, sondern weil Soll und Ist nicht mehr übereinstimmen.
flowchart LR M[Manifest<br/>todo-api.yaml] -->|kubectl apply| A[API-Server] A --> E[(etcd<br/>Zielzustand)] C[Controller-Loop] -->|liest Soll| E C -->|misst Ist| P[Pods im Cluster] C -->|korrigiert Differenz| P
Das erklärt auch, warum kubectl apply -f das zentrale Kommando ist – und nicht eine Choreografie aus „starte dies, dann jenes, dann verdrahte beides". Auf dem alten Server war ein Deployment ein Drehbuch: Shell-Skripte in der richtigen Reihenfolge, und wehe, Schritt vier schlägt fehl, während Schritt drei schon durch ist. In Kubernetes gibt es dieses Drehbuch nicht mehr. Es gibt nur eine Beschreibung des Zielzustands, und apply übergibt sie. Was danach passiert, ist Sache der Controller – auch nachts um zwei.
Der Pod: das, was wirklich läuft
Ein Pod ist die kleinste Einheit, die Kubernetes startet: ein oder mehrere Container, die sich Netzwerk und Lebenszyklus teilen. In der Praxis fast immer ein Container pro Pod. Die zweite Einsicht unter Druck: Pods sind Wegwerfware. Man legt sie selten direkt an, man pflegt sie nicht, man repariert sie nicht. Ein Pod, der stirbt, wird nicht geheilt, sondern ersetzt – mit neuem Namen und neuer IP-Adresse. Wer das akzeptiert, versteht den Rest des Systems fast von allein. Wer dagegen versucht, an einzelnen Pods zu hängen, per kubectl exec hineinzuklettern und Zustand zu retten, kämpft gegen die Plattform statt mit ihr.
Das Deployment: Selbstheilung als Kern-Idee
Weil Pods sterblich sind, braucht es etwas, das sie am Leben hält. Das ist das Deployment – für mich das eigentliche Herzstück:
apiVersion: apps/v1
kind: Deployment
metadata:
name: todo-api
spec:
replicas: 3
selector:
matchLabels:
app: todo-api
template:
metadata:
labels:
app: todo-api
spec:
containers:
- name: todo-api
image: mikebild/todo-api:1.4.2
ports:
- containerPort: 3000
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 15
Seit Kubernetes 1.9 ist apps/v1 die stabile API-Version dafür, das Rätselraten der frühen Beta-Versionen ist vorbei. Das Deployment beschreibt drei Dinge: wie viele Kopien laufen sollen, wie ein Pod auszusehen hat und woran der Cluster erkennt, ob ein Pod gesund ist. Die Readiness-Probe entscheidet, ob ein Pod Traffic bekommt; die Liveness-Probe entscheidet, ob er neu gestartet wird. Diese Unterscheidung habe ich anfangs unterschätzt – bis ein Backend beim Start zwanzig Sekunden für den Datenbank-Verbindungsaufbau brauchte und ohne Readiness-Probe munter Anfragen in ein noch nicht bereites System liefen.
Der Moment, in dem Kubernetes bei mir „klick" gemacht hat, war ein bewusst herbeigeführter Ausfall:
kubectl apply -f todo-api.yaml
kubectl get pods --watch
kubectl delete pod todo-api-7d9f6c8b45-x2x7k # simulate a crash
# a replacement pod appears within seconds
kubectl rollout status deployment todo-api
kubectl rollout undo deployment todo-api # roll back a bad release
Man löscht einen Pod, und bevor man die Ausgabe gelesen hat, steht der Ersatz schon da. Das ist die Antwort auf die Zwei-Uhr-nachts-Frage – nicht als Feature-Versprechen, sondern beobachtbar im Terminal. Und rollout undo macht aus einem misslungenen Release keinen Notfall mehr, sondern einen Schritt zurück zum letzten funktionierenden Zielzustand.
Der Service: eine stabile Adresse vor unstabilen Pods
Wenn Pods ständig kommen und gehen und dabei ihre IP-Adressen wechseln, kann sich niemand direkt mit ihnen verbinden. Der Service löst genau dieses Problem: eine stabile virtuelle Adresse und ein DNS-Name, dahinter eine wechselnde Menge von Pods, ausgewählt über Labels.
apiVersion: v1
kind: Service
metadata:
name: todo-api
spec:
selector:
app: todo-api
ports:
- port: 80
targetPort: 3000
Ab jetzt erreicht jeder Pod im Cluster das Backend unter http://todo-api – egal, welche drei Pods gerade dahinterstehen, egal, wie oft sie ersetzt wurden. Das Frontend der Todo-Anwendung kennt keine IP-Adressen mehr, nur noch diesen Namen. Für mich war das der Punkt, an dem sich die Umzugsarbeit zum ersten Mal gelohnt hat: Service-Discovery, die vorher aus gepflegten Konfigurationsdateien und Hoffnung bestand, ist im Cluster schlicht DNS.
Der Ingress: die Tür nach draußen
Services sind zunächst nur innerhalb des Clusters erreichbar. Für den Weg nach draußen gibt es den Ingress – eine Regel, die Hostnamen und Pfade auf Services abbildet, ausgeführt von einem Ingress-Controller wie ingress-nginx:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: todo-api
spec:
rules:
- host: todo.example.com
http:
paths:
- path: /
backend:
serviceName: todo-api
servicePort: 80
flowchart LR B[Browser] --> I[Ingress<br/>todo.example.com] I --> S[Service<br/>todo-api] S --> P1[Pod] S --> P2[Pod] S --> P3[Pod]
Ehrlicherweise ist der Ingress die unfertigste Ecke dieser Liste: Die Ressource steckt seit Jahren im Beta-Status (extensions/v1beta1), der Controller ist nicht Teil des Clusters, sondern muss separat installiert werden, und je nach Umgebung – Minikube, eigenes Cluster, GKE – verhält sich die Sache leicht unterschiedlich. Trotzdem gehört er in die Grundausstattung, denn ohne Tür nach draußen bleibt die schönste Anwendung ein Selbstgespräch.
ConfigMap und Secret: Konfiguration raus aus dem Image
Der letzte Baustein der Druck-Liste klingt unspektakulär, hat aber die größte Wirkung auf den Alltag: Konfiguration gehört nicht in das Container-Image. Ein Image, das für Staging neu gebaut werden muss, weil eine Datenbank-Adresse anders lautet, ist kein Artefakt, sondern eine Bastelei. ConfigMaps und Secrets ziehen diese Werte aus dem Image heraus in den Cluster:
apiVersion: v1
kind: ConfigMap
metadata:
name: todo-api-config
data:
DATABASE_HOST: "postgres"
LOG_LEVEL: "info"
Im Deployment wird daraus per envFrom ein Satz Umgebungsvariablen, und dasselbe Image läuft unverändert lokal in Minikube und im Cluster beim Cloud-Anbieter. Bei Secrets lohnt ein nüchterner Blick: Die Werte sind Base64-kodiert, nicht verschlüsselt. Das trennt sie sauber von der übrigen Konfiguration und hält sie aus Git heraus, ein Tresor ist es nicht.
Fehlersuche ohne SSH
Was unter Druck ebenfalls sofort gelernt wird: Fehlersuche funktioniert anders als auf einem Server. Es gibt kein SSH auf „die Maschine", weil unklar ist, welche Maschine überhaupt gemeint wäre. Stattdessen fragt man den Cluster, was er über den Zustand weiß:
kubectl describe pod todo-api-7d9f6c8b45-x2x7k
kubectl logs todo-api-7d9f6c8b45-x2x7k
kubectl logs todo-api-7d9f6c8b45-x2x7k --previous # logs of the crashed container
kubectl get events --sort-by=.metadata.creationTimestamp
describe zeigt die Events – dort stehen die Klassiker im Klartext: ImagePullBackOff, wenn das Image nicht gefunden wird, CrashLoopBackOff, wenn der Container beim Start stirbt. Und logs --previous zeigt die Ausgabe des Containers, der gerade abgestürzt ist – genau das, was man um zwei Uhr nachts wissen will. Einfacher als vorher ist das nicht unbedingt, aber es ist systematischer: Der Cluster protokolliert, was er entschieden hat, und man muss es nicht aus vier verschiedenen Logdateien zusammenraten.
Was der Druck nicht wegdiskutiert
Zur Ehrlichkeit gehört auch die andere Seite. Für eine einzige kleine Anwendung stehen jetzt fünf YAML-Dateien im Repository, zusammen deutlich über hundert Zeilen – für etwas, das mit Docker Compose in zwanzig Zeilen beschrieben wäre. Helm verspricht hier Linderung durch Paketierung, bringt aber in Version 2 mit Tiller eine eigene Server-Komponente mit weitreichenden Rechten ins Cluster – ein Tausch von YAML-Menge gegen Betriebs- und Sicherheitsfragen, den ich noch nicht in jedem Projekt eingehen würde. Und auch wenn Amazon seit Juni mit EKS als letzter der großen Anbieter ein verwaltetes Kubernetes anbietet und GKE wie AKS den Cluster-Aufbau abnehmen: Die Konzepte nehmen sie einem nicht ab, und ein selbst per kubeadm aufgesetztes Cluster ist noch einmal eine ganz eigene Betriebsaufgabe.
Deshalb die nüchterne Gegenprobe, bevor der Umzug beginnt. Ein einzelner Host mit Docker Compose reicht schlicht aus, wenn ein paar Dinge zusammenkommen:
- die Anwendung läuft auf einer Maschine, und kurze Ausfälle sind verkraftbar
- eine Restart-Policy im Compose-File deckt den nächtlichen Prozess-Tod ausreichend ab
- es gibt kein Team, das parallel deployt, und keine Last, die Skalierung erfordert
Dann kauft Kubernetes vor allem Komplexität ein und liefert wenig zurück. Der Punkt, an dem die Rechnung kippt, ist erstaunlich präzise benennbar: sobald die Frage „wer startet das neu, wenn niemand hinschaut?" nicht mehr mit „die eine Maschine, hoffentlich" beantwortet werden darf.
Was ich daraus mitnehme
Deployment-Druck ist ein unangenehmer, aber ehrlicher Lehrer. Er hat mir die Konzept-Doku nicht ersetzt, aber er hat sie sortiert: erst der Pod als das, was wirklich läuft, dann das Deployment als Wächter darüber, dann der Service als stabile Adresse, der Ingress als Tür nach draußen, ConfigMap und Secret als ausgelagerte Konfiguration. Fünf Objekte, mit denen eine echte Anwendung produktiv läuft – der Rest des Wörterbuchs kann warten, bis er gebraucht wird.
Die tiefere Einsicht ist die deklarative Denkweise. Der Wechsel vom Drehbuch („führe diese Schritte aus") zum Zielzustand („so soll es aussehen") verändert, wie man über Betrieb nachdenkt – Ausfälle sind keine Zwischenfälle mehr, sondern Differenzen, die ein Controller ausgleicht. Und die Zwei-Uhr-nachts-Frage vom Anfang bekommt damit ihre beste Antwort: Es startet ihn niemand neu. Als der Erste morgens auf das Dashboard schaut, ist der Neustart eine Zeile in den Events – passiert, protokolliert, erledigt.
Weiterführende Quellen
- Repository mit den Beispielen: https://github.com/MikeBild/introduction-kubernetes
- Kubernetes-Dokumentation zu Deployments: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
- Kubernetes-Dokumentation zu Services: https://kubernetes.io/docs/concepts/services-networking/service/
- Kubernetes-Dokumentation zu Ingress: https://kubernetes.io/docs/concepts/services-networking/ingress/
- ConfigMaps in Pods verwenden: https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/
Kommentare