Rolling Updates in Kubernetes: ein Einblick in ReplicaSets und Deployments

Eines der Kern-Features in denen Kubernetes seine Stärken ausspielt sind Rolling Updates im Rahmen von Deployment-Prozessen. Dadurch werden Zero-Downtime-Deployments ermöglicht und dem Endnutzer eine nahtlose User-Experience geboten auch wenn unter der Haube mal eben ein komplett neues Release ausgerollt wird. In diesem Blog-Post soll losgelöst von den konkreten Herausforderungen im Einzelfall, welche bei jeder größeren Applikation zu lösen sind, einmal das grundsätzliche Prinzip im Detail beleuchtet werden, mit welchem Kubernetes dieses Problem löst.
ReplicaSets vs. Deployments
ReplicaSets
ReplicaSets sind die deklarative Abbildung einer Kubernetes-Ressource welche sicherstellt, dass eine definierte Anzahl an Pods von einem bestimmten Typ zu jeder Zeit laufen. Replicasets definieren also den Soll-Zustand einer Applikation und ihrer Skalierung. Oft ist in dem Kontext auch von Replication Controllern die Rede. Diese sind allerdings schon seit Längerem veraltet und die Verwendung von Deployments und ReplicaSets wird empfohlen.
Deployments
Deployments sind eine Abstraktionsebene höher angesiedelt und managen die ReplicaSets, kümmern sich um deren Lifecyle und ermöglichen Updates und Rollbacks.
ReplicaSets
Nehmen wir einmal die folgende Pod-Definition:
# pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
labels:
app: web
tier: frontend
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
Darauf aufbauend würde eine passende ReplicaSet-Definition z.B. wie folgt aussehen:
# replicaset.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: nginx-replicaset
labels:
app: web
tier: frontend
spec:
template:
# hier folgt obige Pod-Deklaration
metadata:
name: nginx-pod
labels:
app: web
tier: frontend
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
replicas: 3
selector:
matchLabels:
app: web
tier: frontend
Anlegen des ReplicaSets:
kubectl create -f replicaset.yaml

Über die Selektoren wird definiert welche Pods dem ReplicaSet zugeordnet sind. Dies erlaubt es auch, dass ReplicaSets auf bestehende Pods mit den selben Labels angewendet werden. Diese werden dann vom Kubernetes-Controller-Manager überwacht. Der Kubernetes-Controller-Manager ist ein Prozess innerhalb des Kubernetes-Clusters welcher diverse Controller verwaltet, darunter auch den ReplicaSet-Controller.
Ändern wir jetzt die Anzahl der Pods des ReplicaSets mit kubectl edit oder kubectl scale, dann passt der Kubernetes-Controller-Manager die Skalierung der Pods entsprechend an:

Was passiert wenn wir jetzt das Container-Image ändern welches in den ReplicaSets definiert ist?

Obwohl das ReplicaSet erfolgreich gepatched wurde, bleiben die Pods unverändert. Das liegt daran, dass ReplicaSets nur den gewünschten Zustand der Replicas, also die der Anzahl der Pods verwalten. Sie lösen aber keinen Neustart der Pods aus wenn sich deren Template ändert, da ReplicaSets keine eingebaute Logik für Rolling Updates haben. Hier kommen die Deployments ins Spiel.
Deployments
Deployments sind wie schon eingangs angedeutet eine Abstraktionsebene höher angesiedelt als ReplicaSets:
Die Definition eines Deployments sieht der eines ReplicaSets sehr ähnlich, in unserem Beispiel ändern sich nur der kind und der name:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: web
tier: frontend
spec:
template:
metadata:
name: nginx-pod
labels:
app: web
tier: frontend
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
replicas: 3
selector:
matchLabels:
app: web
tier: frontend

Es wurden folgende Ressourcen angelegt:
- 3 Pods
- 1 ReplicaSet
- 1 Deployment
Wiederholen wir einmal den Task welchen wir mit dem ReplicaSet schon einmal durchgeführt haben und ändern das Image des ReplicaSets:

Jetzt sehen wir, dass die Änderungen scheinbar sofort angewendet werden, da das Deployment die Änderungen an der Pod-Spezifikation überwacht und per Rolling-Update anwendet. Sehen wir genauer hin, sehen wir auch, dass ein zweites ReplicaSet angelegt wurde. Aber laufen wirklich die richtigen Pods?

Nginx ist noch immer mit dem latest Image ausgerollt, obwohl ein neues ReplicaSet angelegt wurde. Was ist passiert?
Das ReplicaSet, welches wir gepatcht haben, wurde durch das Deployment angelegt und wird auch durch dieses verwaltet. Die Änderung hat also zu einer Inkonsistenz geführt, da das Deployment die Quelle der Wahrheit für das Pod-Template ist. Daher hat das Deployment ein zweites ReplicaSet angelegt, welches wieder dem Stand der Deployment-Spezifikation entspricht.
Wie gehen wir also richtig vor, um ein Deployment zu aktualisieren?
Idealerweise editieren wir das Deployment-File und triggern anschließend einen neuen Rollout mit einer neuen Revision mit kubectl apply:
kubectl apply -f deployment.yaml
Alternativ kann auch der imperative Ansatz gewählt werden. Hierbei wird die Änderung allerdings nicht in der yaml-Definition persistiert:
kubectl set image deployment/nginx-deployment nginx=nginx=1.26.2
Intern geschieht dabei Folgendes:
Zunächst wird ein neues ReplicaSet durch das Kubernetes-Deployment angelegt. Die neuen Container werden in diesem neuen ReplicaSet angelegt und zeitgleich die Container aus dem alten ReplicaSet gelöscht. Abhängig von der Deployment-Strategie werden dabei entweder zunächst alle Container aus dem ersten ReplicaSet gelöscht bevor neue erstellt werden (Recreate-Strategy) oder einer nach dem anderen durch einen neuen ausgetauscht (RollingUpdate-Strategy). Die RollingUpdate-Strategy ist dabei die Default-Strategie:
Nachvollziehen lässt sich das alles auch über kubectl commands: Ausgehend von einem existierenden Deployment, sehen wir dass eine neue Revision in der Rollout-History angelegt wird:

Sehen wir uns das Deployment genau an, sehen wir das neue ReplicaSet und die Anwendung der RollingUpdate-Strategy:
Wendet man hingegen die Recreate-Strategy wie folgt an, ist zu sehen, dass die Pods zunächst auf 0 Replicas runterskaliert werden:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: web
tier: frontend
spec:
template:
metadata:
name: nginx-pod
labels:
app: web
tier: frontend
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
replicas: 3
strategy:
type: Recreate # Recreate-Strategy statt RollingUpdate-Strategy
selector:
matchLabels:
app: web
tier: frontend
Den vorherigen Stand können wir in beiden Fällen über einen rollback auch sehr schnell wiederherstellen:
kubectl rollout undo deployment nginx-deployment
Achtung: hierbei werden die Kubernetes-Ressourcen der vorherigen Revision erneut angewendet und als neue Revision angelegt. Änderungen an persistierten Daten werden hiermit natürlich nicht zurückgerollt.
Liveness-, Readiness- und Startup-Probes
Die Möglichkeit die Pods per RollingUpdate-Strategy auszutauschen ist allerdings nur ein erster Schritt um auch Zero-Downtime fähig zu werden. Um zu verhindern, dass Pods zu früh oder zu spät aus der Verteilung bzw. zu früh wieder in die Lastverteilung genommen werden ist es wichtig messbare Probes zu definieren, welche es Kubernetes ermöglichen das Scheduling optimal und ohne Downtime zu gestalten.
Diese Probes werden vom Kubelet-Prozess, welcher auf jeder Node des Clusters läuft, regelmäßig ausgeführt, um den Zustand der Container zu validieren. Basierend auf dem Ergebnis der Probes werden dann die Deployment-Steps durchlaufen. Kubernetes unterscheidet dabei zwischen:
Liveness-Probes
Prüft regelmäßig ob ein laufender Container noch funktioniert. Bspw. indem ein definierter HTTP-Endpunkt abgefragt und das Ergebnis validiert wird. Schlägt die Probe wiederholt fehl, wird der Container neu gestartet.
Readiness-Probes
Prüft ob ein Container bereit ist Anfragen zu bearbeiten. Schlägt die Readiness-Probe fehl, wird der Container temporär aus der Verteilung genommen aber nicht neu gestartet.
Startup-Probes
Die Startup-Probe prüft ob ein Container erfolgreich gestartet ist. Wenn eine Startup-Probe implementiert ist, werden Readiness- und Liveness-Probe solange deaktiviert, bis die Startup-Probe erfolgreich ist. Dies verhindert bei Containern mit langen Startvorgängen, dass diese während des Startups durch die Liveness Probe neu gestartet werden.
Fazit
Deployments und ReplicaSets ermöglichen uns in Kubernetes die Durchführung von Rolling Updates auf sehr elegante und effiziente Weise. Durch den Einsatz sinnvoll implementierter Probes können wir dabei die Downtime während des Update-Prozesses auf ein absolutes Minimum reduzieren – im Idealfall sogar vollständig vermeiden.