Wat je aan het eind hebt
Een Deployment die nieuwe versies uitrolt zonder ook maar een verbinding te verliezen. De configuratie dekt strategy-tuning, readiness-gating, de endpoint-removal race condition en een PodDisruptionBudget die beschermt tegen voluntary disruptions.
Vereisten
kubectlverbonden met een Kubernetes 1.26+ cluster (1.26 introduceerde hetunhealthyPodEvictionPolicy-veld voor PDBs; oudere versies werken maar missen die functie)- Een bestaande Deployment met minimaal 2 replicas
- Kennis van hoe health probes werken (liveness vs. readiness vs. startup)
- Kennis van resource requests en limits zodat de extra surge pod gescheduled kan worden
RollingUpdate vs. Recreate
Kubernetes ondersteunt twee strategy-types:
RollingUpdate (standaard) maakt pods aan in een nieuwe ReplicaSet terwijl de oude wordt afgeschaald. Op elk moment tijdens de rollout serveert een mix van oude en nieuwe pods het verkeer. Dit is de enige strategie die zero downtime kan opleveren.
Recreate stopt alle bestaande pods voordat nieuwe worden aangemaakt. De service is volledig onbereikbaar tussen het afbreken van de oude en het Ready worden van de nieuwe pods. Gebruik dit alleen wanneer twee versies niet veilig naast elkaar kunnen draaien (een databasemigratie die backwards compatibility breekt, bijvoorbeeld).
Voor stateless HTTP-services is RollingUpdate de juiste keuze. De rest van dit artikel gaat daar vanuit.
Stem maxUnavailable en maxSurge af
Twee velden bepalen hoe snel de rollout verloopt:
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0 # verlaag capaciteit nooit onder het gewenste aantal
maxSurge: 1 # maak een extra pod per keer aan
maxUnavailable bepaalt het maximale aantal pods dat unavailable mag zijn tijdens de update. Accepteert een integer of percentage (percentages worden naar beneden afgerond). maxSurge bepaalt hoeveel extra pods er mogen bestaan boven het gewenste aantal (percentages worden naar boven afgerond). Kubernetes verbiedt om beide op 0 te zetten.
De defaults zijn 25% voor allebei. Bij 8 replicas betekent dat: maximaal 2 pods unavailable en maximaal 2 extra pods tegelijk. Dat is agressief genoeg om merkbare capaciteitsdips te veroorzaken.
Zet voor user-facing services maxUnavailable: 0. Zo is de volledige gewenste capaciteit altijd beschikbaar. Geen oude pod wordt verwijderd totdat een nieuwe Ready is.
| Patroon | maxUnavailable | maxSurge | Afweging |
|---|---|---|---|
| Veiligst, langzaamst | 0 | 1 | Volledige capaciteit altijd; een pod per keer |
| Veilig, sneller | 0 | 25% | Volledige capaciteit; meerdere surge pods versnellen rollout |
| Gebalanceerd (standaard) | 25% | 25% | Korte capaciteitsdip mogelijk; prima voor niet-kritieke services |
Houd er rekening mee dat maxSurge extra node-capaciteit vereist. 100% verdubbelt het aantal pods tijdelijk.
Voeg een stabiliteitsbuffer toe met minReadySeconds
spec:
minReadySeconds: 10
Een pod telt pas als Available nadat hij minstens minReadySeconds continu Ready is geweest. Dit vangt applicaties op die de readiness probe even halen maar seconden later crashen. De default is 0, wat betekent dat de rollout doorgaat zodra de readiness probe voor het eerst slaagt.
Stuur verkeer via readiness probes
Zonder readiness probe stuurt Kubernetes verkeer naar een pod zodra de container start. Bij een rolling update gaan er dan requests naar een applicatie die nog niet klaar is met opstarten.
readinessProbe:
httpGet:
path: /healthz/ready
port: 8080
initialDelaySeconds: 5 # stem af op de opstarttijd van je app
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
successThreshold: 1
De readiness probe bepaalt of een pod wordt opgenomen in de Service EndpointSlices. Tijdens een rolling update moet de readiness probe van een nieuwe pod slagen voordat Kubernetes hem als Available beschouwt en voordat oude pods worden beëindigd (bij maxUnavailable 0).
Voor applicaties met langzame of onvoorspelbare opstarttijden (JVM warm-up, modelbestanden laden): voeg een startup probe toe in plaats van initialDelaySeconds op te blazen. De startup probe blokkeert readiness- en liveness-checks totdat het opstarten voltooid is.
Draai je achter een cloud load balancer (AWS ALB/NLB, GCP GCLB)? Overweeg dan Pod Readiness Gates. De ALB-controller injecteert een custom readiness-conditie die pas True wordt wanneer de target group de pod als Healthy registreert. Zonder dat kan de rollout oude pods beëindigen voordat de load balancer überhaupt verkeer naar de nieuwe stuurt.
Wil je dieper in probe-types, mechanismen en timing-valkuilen duiken? Zie hoe je Kubernetes health probes configureert.
Los de endpoint-removal race condition op
Dit is veruit de meest voorkomende oorzaak van 502-fouten tijdens deployments. Wanneer Kubernetes besluit een pod te beëindigen, starten twee onafhankelijke processen tegelijkertijd:
- De kubelet voert de preStop hook uit en stuurt daarna SIGTERM.
- De endpoint controller verwijdert het IP van de pod uit de EndpointSlices, waardoor kube-proxy op elke node zijn iptables/IPVS-regels bijwerkt.
Die twee paden zijn niet gesynchroniseerd. De kube-proxy minSyncPeriod staat standaard op 1 seconde, maar de daadwerkelijke propagatie over alle nodes kan meerdere seconden duren. Gedurende dat venster routeert kube-proxy op sommige nodes nog steeds nieuwe verbindingen naar de terminerende pod. Als de applicatie al gestopt is met het accepteren van verbindingen, falen die requests.
De oplossing is een preStop sleep die de pod lang genoeg in leven houdt totdat de endpoint-verwijdering overal is doorgevoerd:
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
Tijdens die 15 seconden accepteert de pod nog gewoon verbindingen terwijl alle kube-proxies bijwerken. Na de sleep stuurt Kubernetes SIGTERM en begint de applicatie met afsluiten.
Hoe lang moet de preStop sleep zijn?
| Omgeving | Aanbevolen sleep | Waarom |
|---|---|---|
| Eenvoudig on-prem cluster | 5 tot 10 seconden | Snelle iptables-propagatie |
| Cloud met externe load balancer (ALB/NLB) | 35+ seconden | Cloud-LBs doen 15 tot 30 seconden over target-deregistratie |
| Service mesh (Istio, Linkerd) | 5 tot 10 seconden + mesh drain | Envoy-sidecar heeft een eigen drain-cyclus |
Budget terminationGracePeriodSeconds correct
De timer begint wanneer de preStop hook start, niet nadat hij klaar is. Je budget moet dus zijn:
terminationGracePeriodSeconds >= preStop_sleep + applicatie_shutdown_tijd + buffer
Met een preStop sleep van 15 seconden en een applicatie die 10 seconden nodig heeft om verbindingen te drainnen:
terminationGracePeriodSeconds: 30 # 15 + 10 + 5 buffer
Als de timer afloopt voordat de applicatie stopt, stuurt Kubernetes SIGKILL en worden alle lopende requests afgebroken.
Wil je alles weten over SIGTERM-handling, connection draining in Go/Node.js/Java/Python, en het testen van je shutdown? Zie Kubernetes graceful shutdown.
Bescherm tegen voluntary disruptions met PodDisruptionBudgets
Een rolling update is niet het enige dat pods beëindigt. kubectl drain, cluster autoscaler scale-down en cloud provider node pool upgrades zijn voluntary disruptions die pods onafhankelijk van de Deployment controller kunnen evicten. Zonder PodDisruptionBudget kan een kubectl drain al je pods tegelijk verwijderen.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: myapp-pdb
spec:
maxUnavailable: 1
unhealthyPodEvictionPolicy: AlwaysAllow
selector:
matchLabels:
app: myapp
maxUnavailable: 1 staat toe dat voluntary disruptions een pod per keer verwijderen. Voor hogere replica-aantallen is maxUnavailable: 25% een goed startpunt.
Het unhealthyPodEvictionPolicy: AlwaysAllow-veld (Kubernetes 1.26+) voorkomt dat PDBs node drains blokkeren wanneer pods al in een crash-loop zitten. Zonder dit veld telt een kapotte pod die nooit Ready is mee voor het disruption budget, waardoor kubectl drain eindeloos blijft hangen.
Zet maxUnavailable niet op 0, tenzij je een quorum-gebaseerd systeem draait dat echt geen enkel lid mag verliezen. Het blokkeert alle voluntary evictions, inclusief routine clusteronderhoud.
PDBs en rolling updates zijn onafhankelijk. PDBs bepalen niet hoe de Deployment controller pods uitrolt. Ze beschermen tegen externe disruptions die toevallig samenvallen met een rollout.
Blue/green en canary als alternatieven
Rolling updates werken goed voor de meeste stateless services. Twee patronen bieden extra controle wanneer je dat nodig hebt.
Blue/green
Draai twee volledige Deployments (blue en green) achter een enkele Service. Wissel verkeer door de Service selector te patchen:
# Schakel over naar green
kubectl patch service myapp -p '{"spec":{"selector":{"version":"green"}}}'
# Directe rollback naar blue
kubectl patch service myapp -p '{"spec":{"selector":{"version":"blue"}}}'
Directe rollback. Geen versiemix. Maar het kost 2x de resources en ondersteunt geen geleidelijke traffic-verschuiving.
Canary
Ship de nieuwe versie eerst naar een klein percentage van het verkeer. Native Kubernetes ondersteunt grove canary via replica-verhoudingen (9 stabiele pods + 1 canary pod = circa 10% canary-verkeer). Voor precieze percentages gebruik je NGINX Ingress canary annotations of een dedicated controller:
- Argo Rollouts vervangt de Deployment door een
RolloutCRD met step-based canary, analysis-driven promotie en automatische rollback op basis van Prometheus- of Datadog-metrics. - Flagger werkt met bestaande Deployments (geen manifest-migratie nodig) en regelt traffic-shifting via Ingress controllers of service meshes.
Beide zijn het evalueren waard zodra je team native rolling updates ontgroeit.
Compleet productieklaar manifest
Dit manifest combineert alle instellingen die hierboven zijn besproken. Inline comments leggen elk veld uit.
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 4
revisionHistoryLimit: 5 # bewaar 5 oude ReplicaSets voor rollback
progressDeadlineSeconds: 300 # markeer rollout als mislukt na 5 minuten zonder voortgang
minReadySeconds: 10 # pod moet 10s Ready zijn voor hij als Available telt
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0 # verlaag capaciteit nooit onder 4
maxSurge: 1 # voeg een pod per keer toe
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
terminationGracePeriodSeconds: 60 # preStop 15s + app drain 30s + 15s buffer
containers:
- name: app
image: registry.internal/myapp:v2.4.1
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /healthz/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
successThreshold: 1
livenessProbe:
httpGet:
path: /healthz/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
memory: "256Mi" # geen CPU-limit; voorkom onnodige throttling
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: myapp
topologyKey: kubernetes.io/hostname
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: myapp-pdb
spec:
maxUnavailable: 1
unhealthyPodEvictionPolicy: AlwaysAllow
selector:
matchLabels:
app: myapp
Controleer het resultaat
Na het toepassen van het manifest, bevestig dat de rollout zonder fouten voltooit:
kubectl rollout status deployment/myapp --timeout=5m
Verwachte output:
deployment "myapp" successfully rolled out
Bekijk tijdens de rollout de pod-transities in een tweede terminal:
kubectl get pods -l app=myapp -w
Je zou moeten zien dat nieuwe pods 1/1 Running bereiken voordat oude pods Terminating worden. Op geen enkel moment mag het aantal Running, Ready pods onder je gewenste replica-aantal zakken.
Om te verifiëren dat er nul verbindingen zijn gedropt, draai een load test tegen de Service tijdens een rollout. Tools als hey, wrk of k6 werken goed. Een geslaagde zero-downtime deployment levert 0 non-2xx responses op over het hele rollout-venster.
Veelvoorkomende problemen
Rollout hangt en geen nieuwe pods worden Ready. De readiness probe faalt op nieuwe pods. Check kubectl describe pod <nieuwe-pod> voor probe failure events. De meest voorkomende oorzaak is een verkeerd path of port in de readiness probe, of een initialDelaySeconds die korter is dan de daadwerkelijke opstarttijd.
502-fouten tijdens deploy ondanks werkende readiness probes. De endpoint-removal race condition. De preStop hook ontbreekt of de sleep is te kort voor je omgeving. Voeg de preStop sleep toe of verhoog hem. Achter een cloud load balancer is 35+ seconden normaal.
kubectl drain blokkeert eindeloos. Een PDB voorkomt eviction. Of maxUnavailable: 0 is ingesteld (nul disruptions toegestaan), of unhealthy pods verbruiken het budget. Zet unhealthyPodEvictionPolicy: AlwaysAllow (Kubernetes 1.26+) om drains op crashende pods te deblokkeren.
ProgressDeadlineExceeded-conditie verschijnt. De rollout heeft geen voortgang geboekt binnen progressDeadlineSeconds. Kubernetes doet geen automatische rollback. Rollback handmatig met kubectl rollout undo deployment/myapp en onderzoek waarom nieuwe pods niet Available worden.
Surge pods blijven in Pending hangen. Het cluster heeft niet genoeg node-capaciteit voor de extra pod. Verlaag maxSurge, voeg nodes toe, of bekijk je resource requests om te zien of ze overprovisioned zijn.