Ein Kubernetes Deployment beschreibt, welche Pods in welcher Anzahl laufen sollen, und sorgt dafür, dass der Cluster genau diesen Zustand herstellt und hält. Stirbt ein Pod, kommt ein neuer. Änderst du das Container-Image, tauscht das Deployment die Pods Schritt für Schritt aus, ohne dass deine Anwendung dabei ausfällt. Und wenn die neue Version nicht läuft, bringt es dich mit einem Befehl zur vorherigen zurück.
Das Deployment ist damit das Objekt, mit dem du in Kubernetes fast alle zustandslosen Anwendungen betreibst: Webserver, APIs, Worker. Wer nur Pods versteht, hat Kubernetes noch nicht benutzt. Wer das Deployment versteht, hat den Kern verstanden.
Ich betreibe Kubernetes-Cluster in Produktion, rolle darüber jede Woche neue Versionen aus und habe dem Thema im dritten Kapitel meines Kubernetes-Buchs bei Rheinwerk einen eigenen Abschnitt gewidmet. Hier steht die Kurzfassung mit den Stolperfallen, die ich selbst kenne. Alle meine Beiträge zu Kubernetes findest du gesammelt auf der Seite Kubernetes.
Pod, ReplicaSet, Deployment: wer verwaltet wen
Ein einzelner Pod in Kubernetes hat ein Problem: Er ist sterblich. Fällt die Node aus oder stürzt der Container endgültig ab, ist der Pod weg und niemand erzeugt ihn neu. Für eine Anwendung, die dauerhaft laufen soll, brauchst du deshalb ein Objekt, das über den Pods steht und sie überwacht.
Das erledigt in Kubernetes das ReplicaSet. Es kennt ein Pod-Template, eine gewünschte Anzahl und einen Label-Selektor. Alle paar Sekunden vergleicht es, wie viele Pods mit dem passenden Label tatsächlich laufen, mit der Zahl, die es haben soll. Fehlt einer, erzeugt es aus dem Template einen neuen. Läuft einer zu viel, beendet es ihn. Mehr kann ein ReplicaSet nicht, und mehr muss es auch nicht können.
Das Deployment sitzt eine Ebene darüber. Es erzeugt und besitzt das ReplicaSet und bringt die Logik mit, die dem ReplicaSet fehlt: Versionen ausrollen, den Rollout überwachen, pausieren und zurückrollen. Bei jeder Änderung am Pod-Template legt das Deployment ein neues ReplicaSet an, skaliert es hoch und das alte parallel herunter. Die alten ReplicaSets bleiben leer stehen und sind dein Rollback-Gedächtnis.
|
Pod |
ReplicaSet |
Deployment |
| Läuft eine Anwendung? |
ja, genau eine Instanz |
ja, in gewünschter Anzahl |
ja, in gewünschter Anzahl |
| Ersetzt ausgefallene Instanzen |
nein |
ja |
ja, über das ReplicaSet |
| Rollt neue Versionen kontrolliert aus |
nein |
nein |
ja |
| Rollback auf eine ältere Version |
nein |
nein |
ja |
| Wird direkt angelegt |
nur zum Ausprobieren |
praktisch nie |
ja, als Standard |
Technisch hängen die drei über Owner-Referenzen zusammen. Jeder Pod trägt in seinen Metadaten einen Verweis auf das ReplicaSet, das ihn erzeugt hat, und das ReplicaSet einen Verweis auf das Deployment. Löschst du das Deployment, räumt Kubernetes das ReplicaSet und die Pods automatisch mit ab. Die Kubernetes-Dokumentation beschreibt das unter Owners and Dependents.
Aus dieser Besitzkette folgt eine Regel, über die viele am Anfang stolpern: Änderungen direkt am ReplicaSet sind sinnlos. Erhöhst du dort die Replicas, setzt das Deployment den Wert innerhalb von Sekunden wieder auf seinen eigenen zurück. Das Deployment ist die einzige Wahrheit, alles darunter ist abgeleitet.
Kubernetes Deployment YAML: das Manifest Zeile für Zeile
Ein Deployment-Manifest sieht dem Pod-Manifest zum Verwechseln ähnlich, denn das Pod-Template ist darin enthalten. Neu sind die Felder replicas, selector und strategy. Das folgende Beispiel ist vollständig und läuft in jedem Cluster:
apiVersion: apps/v1
kind: Deployment
metadata:
name: shop-api
labels:
app: shop-api
spec:
replicas: 3
selector:
matchLabels:
app: shop-api
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
template:
metadata:
labels:
app: shop-api
spec:
containers:
- name: api
image: nginx:stable
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 5
replicas legt fest, wie viele Pods gleichzeitig laufen sollen. selector.matchLabels sagt dem ReplicaSet, welche Pods zu ihm gehören, und muss exakt zu den Labels unter template.metadata.labels passen, sonst lehnt der API-Server das Manifest ab. Alles unter template ist die Pod-Spezifikation, die du schon kennst. strategy bestimmt, wie ein Update abläuft, dazu gleich mehr. Die Readiness Probe ist keine Pflicht, aber ohne sie ist ein Rollout ohne Ausfall Glückssache, deshalb gehört sie bei mir in jedes Deployment.
Rollst du das Manifest mit kubectl apply -f deployment.yaml aus, siehst du kurz darauf alle drei Ebenen:
$ kubectl get deployment,replicaset,pods -l app=shop-api
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/shop-api 3/3 3 3 40s
NAME DESIRED CURRENT READY AGE
replicaset.apps/shop-api-7d4b9c6f5 3 3 3 40s
NAME READY STATUS RESTARTS AGE
pod/shop-api-7d4b9c6f5-2xk8p 1/1 Running 0 40s
pod/shop-api-7d4b9c6f5-9tqzn 1/1 Running 0 40s
pod/shop-api-7d4b9c6f5-m5vwd 1/1 Running 0 40s
Der Suffix 7d4b9c6f5 ist der Hash des Pod-Templates. Jedes ReplicaSet bekommt seinen eigenen, und daran erkennst du später auf einen Blick, welcher Pod zu welcher Version gehört.
Ein Hinweis zu dem Selektor: Er ist nach dem Anlegen unveränderlich. Wenn du die Labels deiner Pods später umbenennen willst, musst du das Deployment löschen und neu anlegen. Wähle den Selektor deshalb sparsam, meistens reicht ein einziges app-Label.
Skalieren: replicas ändern statt Pods anlegen
Mehr Instanzen bekommst du nie, indem du Pods anlegst, sondern indem du die Zahl im Deployment änderst. Der schnelle Weg ist kubectl scale deployment/shop-api --replicas=5. Das ReplicaSet merkt die Differenz und startet zwei zusätzliche Pods aus dem Template. Skalierst du wieder herunter, beendet es die überzähligen.
Der schnelle Weg ist in Produktion aber selten der richtige. In meinen Clustern liegt jedes Deployment als Manifest in Git und wird per GitOps ausgerollt. Ändere ich die Replicas per kubectl scale, weicht der Cluster vom Repository ab, und beim nächsten Sync steht der alte Wert wieder da. Wer deklarativ arbeitet, ändert die Zahl im Manifest und lässt die Pipeline ausrollen. Für automatisches Skalieren nach Last gibt es den Horizontal Pod Autoscaler, der genau dieses Feld für dich anpasst.
Rolling Update: maxSurge und maxUnavailable verstehen
Änderst du irgendetwas am Pod-Template, meist das Image, startet das Deployment einen Rollout. Wie es dabei vorgeht, steuert das Feld strategy, in der Dokumentation auch Deployment Strategy genannt. Kubernetes kennt zwei Varianten:
|
Recreate |
RollingUpdate |
| Ablauf |
alle alten Pods löschen, dann alle neuen starten |
alte und neue Pods überlappend austauschen |
| Ausfallzeit |
ja, immer |
keine, wenn die Anwendung mitspielt |
| Zwei Versionen gleichzeitig aktiv |
nie |
ja, kurzzeitig |
| Sinnvoll für |
Entwicklungscluster, Anwendungen, die nur einmal laufen dürfen |
alles, was zustandslos und horizontal skalierbar ist |
| Standard |
nein |
ja |
Beim Rolling Update legt das Deployment ein zweites ReplicaSet mit dem neuen Template an und verschiebt die Pods stückweise: neues ReplicaSet um eins hoch, altes um eins runter, warten bis der neue Pod ready ist, weiter. Wie groß die Schritte sind, bestimmen zwei Werte. maxUnavailable sagt, wie viele Pods während des Updates unter der Sollzahl fehlen dürfen. maxSurge sagt, wie viele Pods über der Sollzahl zusätzlich laufen dürfen. Beide nehmen eine feste Zahl oder einen Prozentwert, Standard ist jeweils 25 Prozent. Bei Prozenten rundet Kubernetes maxUnavailable ab und maxSurge auf.
Mit den Werten aus dem Manifest oben, drei Replicas und jeweils 1, heißt das: Nie weniger als zwei Pods verfügbar, nie mehr als vier gleichzeitig vorhanden. Der Rollout läuft dann in Schritten: Ein neuer Pod startet, und sobald er ready ist, wird ein alter beendet. Danach der nächste. Nach drei solchen Schritten laufen nur noch Pods der neuen Version, und zu keinem Zeitpunkt waren weniger als zwei ansprechbar.
Zwei Einstellungen aus der Praxis: maxUnavailable: 0 mit maxSurge: 1 ist die konservative Variante für Anwendungen, bei denen jede Instanz zählt, kostet aber Platz für einen zusätzlichen Pod. Bei knappen Nodes, auf denen kein weiterer Pod mehr passt, geht es umgekehrt, maxSurge: 0 mit maxUnavailable: 1, und dann läuft die Anwendung während des Updates kurz mit einer Instanz weniger.
Ohne Readiness Probe kein Rollout ohne Ausfall
Das Rolling Update funktioniert nur so gut wie die Antwort auf die Frage: Wann ist ein neuer Pod fertig? Ohne Readiness Probe gilt ein Pod als ready, sobald seine Container gestartet sind. Für eine Anwendung, die nach dem Start erst zehn Sekunden lang Konfiguration lädt oder eine Datenbankverbindung aufbaut, ist das zu früh. Kubernetes beendet dann den nächsten alten Pod, während der neue noch keine Anfragen beantworten kann, und deine Nutzer sehen Fehler.
Mit einer Readiness Probe wartet das Deployment, bis die Anwendung selbst meldet, dass sie bereit ist. Erst dann rückt der Rollout weiter. Der zweite Vorteil ist noch wichtiger: Startet die neue Version gar nicht, wird der neue Pod nie ready, das Deployment kommt nicht über den ersten Schritt hinaus, und die alten Pods bleiben stehen. Ein kaputtes Release erreicht so nie den ganzen Cluster. Genau dieser Fall führt uns zum hängenden Rollout.
Rollout beobachten, hängendes Rollout erkennen, Rollback auslösen
Nach jedem Update schaue ich mit kubectl rollout status zu. Bei einem gesunden Rollout endet der Befehl nach wenigen Sekunden mit einer Erfolgsmeldung. Bei einem kaputten Image sieht das so aus:
$ kubectl rollout status deployment/shop-api
Waiting for deployment "shop-api" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "shop-api" rollout to finish: 1 out of 3 new replicas have been updated...
error: deployment "shop-api" exceeded its progress deadline
$ kubectl get pods -l app=shop-api
NAME READY STATUS RESTARTS AGE
shop-api-7d4b9c6f5-9tqzn 1/1 Running 0 14m
shop-api-7d4b9c6f5-m5vwd 1/1 Running 0 14m
shop-api-98f6d7c4b-x2lrq 0/1 CrashLoopBackOff 6 10m
Die Ausgabe erzählt die ganze Geschichte. Zwei alte Pods laufen weiter, der eine neue Pod stürzt beim Start ab und wird nie ready, deshalb rückt das Deployment nicht weiter. Der Fehler nach zehn Minuten kommt von progressDeadlineSeconds, standardmäßig 600 Sekunden. Nach dieser Frist markiert das Deployment den Rollout als fehlgeschlagen, tut aber von sich aus nichts weiter. Es rollt nicht zurück, es wartet.
Was der neue Pod genau hat, verrät kubectl describe pod in den Events und kubectl logs für den Container. Häufige Ursachen in meinen Projekten: ein Image-Tag, den es in der Registry nicht gibt, ein fehlendes Secret, das als Umgebungsvariable erwartet wird, oder eine Readiness Probe, die auf einen Pfad zeigt, den die neue Version umbenannt hat.
Für den Weg zurück gibt es die Revisionsliste des Deployments und den Undo-Befehl:
$ kubectl rollout history deployment/shop-api
REVISION CHANGE-CAUSE
1 <none>
2 <none>
$ kubectl rollout undo deployment/shop-api
deployment.apps/shop-api rolled back
kubectl rollout undo skaliert das vorherige ReplicaSet wieder hoch und das kaputte herunter, nach denselben Regeln wie ein normales Rolling Update. Mit --to-revision springst du gezielt zu einer älteren Revision. Wie viele davon Kubernetes aufhebt, steuert revisionHistoryLimit, standardmäßig zehn. Setzt du den Wert auf null, gibt es keinen Rollback mehr.
Ein Rat aus dem Betrieb: Der Rollback per kubectl ist die Notbremse, nicht der Prozess. Nach einem rollout undo stimmt der Cluster nicht mehr mit deinem Git-Repository überein, und die nächste Person, die synchronisiert, rollt das kaputte Release erneut aus. In meinen Projekten läuft ein Rollback deshalb als Revert im Repository, den die Pipeline ausrollt. Die Details, auch zu Pause und Resume während eines Rollouts, stehen in Rolling Update und Rollback in Kubernetes.
Kubernetes Deployment vs. StatefulSet vs. DaemonSet
Das Deployment ist der richtige Controller für zustandslose Anwendungen, bei denen jeder Pod jeden anderen ersetzen kann. Sobald Pods eine feste Identität oder eigenen Speicher brauchen oder auf jeder Node genau einmal laufen sollen, passt ein anderes Objekt besser.
|
Deployment |
StatefulSet |
DaemonSet |
| Pod-Namen |
zufälliger Hash |
fest nummeriert (db-0, db-1) |
pro Node einer |
| Eigener Speicher je Pod |
nein, alle teilen |
ja, je Pod ein eigenes Volume |
selten |
| Reihenfolge bei Start und Update |
beliebig, parallel |
geordnet, einer nach dem anderen |
je Node |
| Anzahl bestimmt durch |
replicas |
replicas |
Anzahl der Nodes |
| Typische Anwendung |
Webserver, API, Worker |
Datenbank, Message Broker, Cache |
Log-Agent, Monitoring, Netzwerk-Plugin |
Die häufigste Fehlentscheidung, die ich sehe, ist eine Datenbank im Deployment mit einem angehängten Volume. Solange nur ein Replica läuft, geht das gut. Beim ersten Rolling Update startet Kubernetes den zweiten Pod, bevor der erste weg ist, und beide wollen dasselbe Volume. Für alles, was einen Zustand hält, ist das StatefulSet in Kubernetes gebaut.
Häufige Fragen
Was ist der Unterschied zwischen Deployment und ReplicaSet?
Das ReplicaSet hält eine feste Zahl gleicher Pods am Leben, mehr nicht. Das Deployment verwaltet ReplicaSets und bringt Rollouts, Rollback und Pause dazu. Bei jeder neuen Version legt es ein neues ReplicaSet an. Im Alltag legst du nur Deployments an; ReplicaSets entstehen automatisch.
Rolling Update oder Recreate, was soll ich nehmen?
RollingUpdate, solange deine Anwendung mehrfach parallel laufen kann und keinen lokalen Zustand hält. Recreate nur dann, wenn zwei Versionen gleichzeitig Schaden anrichten würden, etwa bei einer Datenbank-Migration, die nicht abwärtskompatibel ist, oder in einem Testcluster, in dem kein Platz für einen zusätzlichen Pod ist.
Warum hängt mein Rollout?
Fast immer, weil der neue Pod nicht ready wird: falsches Image-Tag, fehlendes Secret oder eine ConfigMap, Absturz beim Start, oder eine Readiness Probe, die nie erfolgreich ist. Schau mit kubectl get pods auf den Status des neuen Pods, dann mit kubectl describe pod in die Events und mit kubectl logs in die Anwendung. Das Deployment wartet so lange, bis du die Ursache behebst oder zurückrollst.
Wann brauche ich ein StatefulSet statt eines Deployments?
Wenn jeder Pod einen eigenen, dauerhaften Speicher braucht, einen stabilen Namen haben muss oder in fester Reihenfolge starten soll. Das trifft auf Datenbanken, Message Broker und verteilte Caches zu. Für Webserver, APIs und Worker ohne lokalen Zustand ist das Deployment die bessere und einfachere Wahl.
Wie mache ich einen Rollback auf die letzte Version?
Mit kubectl rollout undo deployment/<name> gehst du eine Revision zurück, mit --to-revision=<n> zu einer bestimmten. Kubernetes skaliert dann das alte ReplicaSet wieder hoch. Danach solltest du dein Manifest im Repository ebenfalls zurücksetzen, sonst rollt der nächste Sync die kaputte Version erneut aus.
Wie du weitermachst
Wenn du das Deployment verstanden hast, kennst du den Mechanismus, mit dem Kubernetes praktisch jede Anwendung ausrollt. Was im Pod-Template steht, erklärt der Beitrag Kubernetes Pod: was ist ein Pod?. Wie du Rollouts in Produktion pausierst, prüfst und sauber zurücknimmst, steht in Rolling Update und Rollback in Kubernetes. Und wann du statt eines Deployments ein StatefulSet brauchst, zeigt Kubernetes StatefulSet vs Deployment.
Mein Vorschlag für heute: Nimm das Manifest von oben, rolle es aus, ändere das Image auf einen Tag, den es nicht gibt, und beobachte mit kubectl rollout status und kubectl get pods, wie das Deployment stehen bleibt und die alten Pods weiterlaufen. Dann rollout undo. Wer das einmal gesehen hat, vertraut dem Mechanismus. Die vollständige Referenz aller Felder findest du in der Deployment-Dokumentation von Kubernetes.
Ausführlich mit allen Beispielen, vom ReplicaSet-Experiment bis zum Diff zweier Revisionen, steht das in Kapitel 3 meines Buchs „Kubernetes: Das Praxisbuch für Entwickler und DevOps-Teams" (Rheinwerk). Alle Informationen zum Buch findest du auf meiner Kubernetes-Seite.