Doel
Een node wordt uit de scheduling gehaald, alle workload erop wordt netjes gemigreerd naar andere nodes, het onderhoud (kernelpatch, schijfwissel, kubelet-upgrade, decommissioning) draait op een lege node, en daarna doet de node weer mee of wordt vervangen. Tijdens de hele procedure merkt geen enkele eindgebruiker er iets van, omdat PodDisruptionBudgets en graceful termination de servingcapaciteit van de applicatie intact houden.
Vereisten
kubectlverbonden met een Kubernetes 1.27 of nieuwer cluster (de Eviction API en depolicy/v1PDB-API zijn stabiel in alle nu ondersteunde releases)- De naam van de node die je wilt drainen (
kubectl get nodes) - Clusterrechten om pods te evicten (
pods/evictioncreate) en nodes te patchen (nodespatch) - Voor workloads die je wilt beschermen: een PodDisruptionBudget op elke Deployment of StatefulSet die je belangrijk vindt. Zonder PDB kan drain rustig al je replica's tegelijk verwijderen.
- Genoeg vrije capaciteit op de rest van het cluster om de gemigreerde pods te hosten. Zit het cluster vol, dan blijven die pods in
Pendinghangen en verliest de applicatie alsnog capaciteit.
Wat cordon en drain echt doen (en wat niet)
De twee commando's lijken op elkaar en worden vrijwel altijd door elkaar gehaald, maar ze hebben totaal andere reikwijdte. Het verschil snappen is de helft van de hele procedure.
kubectl cordon <node> wijzigt precies één veld op het node-object: .spec.unschedulable: true. Onder water voegt de node-controller de node.kubernetes.io/unschedulable:NoSchedule taint toe. Nieuwe pods worden er niet meer geplaatst. Bestaande pods op de node blijven gewoon draaien. Er wordt niks geëvict, niks geherstart, niks verandert voor de huidige workload.
kubectl drain <node> doet twee dingen: eerst cordont het de node, daarna loopt het door elke pod op die node en stuurt per pod een Eviction API request. Elke eviction loopt langs hetzelfde admissiepad als een gewone pod-deletion: PodDisruptionBudgets worden gecheckt, terminationGracePeriodSeconds wordt gerespecteerd, lifecycle preStop-hooks vuren. Als de PDB de eviction afwijst, geeft de API een HTTP 429 terug en kubectl drain blijft retryen tot het lukt of de --timeout afloopt.
Drie dingen die drain expliciet niet doet:
- Het verwijdert de node niet uit het cluster. Na een geslaagde drain staat het Node-object er nog steeds, alleen met
Unschedulable: true. Om de node weer te activeren draai jekubectl uncordon. Om hem permanent te verwijderen iskubectl delete nodeeen aparte stap. - Het stopt de kubelet niet, reboot de host niet, en raakt de onderliggende VM niet aan. Dat is jouw werk; drain haalt alleen de workload eraf.
- Het evict geen DaemonSet-pods. Die horen bij een controller die ze op elke node opnieuw aanmaakt, dus evicten heeft geen zin. Standaard weigert drain daarom te starten; je geeft
--ignore-daemonsetsmee om dat te bevestigen.
Een veelgemaakte denkfout in incident-reviews: het team draaide kubectl delete node en dacht dat de workload zo verhuisd was. Die is niet verhuisd; de pods zijn ruw geweesd en als nieuwe pods elders herstart, zonder graceful shutdown, zonder preStop-hook en zonder respect voor je PDB. kubectl drain is de enige manier om workload veilig van een node af te halen.
De onderhoudsflow: cordon, drain, onderhoud, uncordon
Het patroon van vier stappen dat werkt voor kernelupgrades, kubelet-upgrades, schijfwissels en nodevervanging:
# 1. Geen nieuwe pods meer naar de node (direct, niet-disruptief)
kubectl cordon worker-3
# 2. Bestaande pods evicten via de Eviction API
kubectl drain worker-3 \
--ignore-daemonsets \
--delete-emptydir-data \
--grace-period=-1 \
--timeout=15m
# 3. Onderhoud draaien op de inmiddels lege node
# (kernelpatch, kubelet-upgrade, schijfwissel, etc.)
# 4. Node weer in roulatie brengen
kubectl uncordon worker-3
De reden dat cordon als losse stap voor drain staat (drain cordont intern toch al), is om je een schoon reviewmoment te geven. Na stap 1 kun je rustig kubectl get pods -o wide --field-selector spec.nodeName=worker-3 draaien om te zien wat er straks geëvict gaat worden, je PDBs nalopen en checken of het cluster wel ruimte heeft, voordat je de drain echt commit.
Stap 1 verifiëren. De node staat nu als SchedulingDisabled naast Ready:
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
worker-1 Ready <none> 42d v1.30.4
worker-2 Ready <none> 42d v1.30.4
worker-3 Ready,SchedulingDisabled <none> 42d v1.30.4
Stap 2 verifiëren. Als drain klaar is, draaien er nul pods meer op de node behalve de DaemonSet-managed pods (CNI, monitoringagent, log shipper):
$ kubectl get pods -o wide --field-selector spec.nodeName=worker-3
NAME READY STATUS RESTARTS AGE NODE
calico-node-7p9zx 1/1 Running 0 42d worker-3
fluent-bit-x4c2m 1/1 Running 0 42d worker-3
node-exporter-bbm49 1/1 Running 0 42d worker-3
Stap 4 verifiëren. Het SchedulingDisabled-label is weg en de scheduler plaatst weer pods op de node:
$ kubectl get nodes worker-3
NAME STATUS ROLES AGE VERSION
worker-3 Ready <none> 42d v1.30.4
Bij een geplande nodevervanging vervang je stap 3 door "VM verwijderen, je node-pool autoscaler of cloud controller maakt een nieuwe aan", en stap 4 sla je over want de nieuwe node komt direct schedulable binnen.
Veelvoorkomende blokkade: DaemonSet-pods blokkeren drain (--ignore-daemonsets)
De eerste keer dat je kubectl drain draait, loop je hier tegenaan:
error: unable to drain node "worker-3" due to error: cannot delete DaemonSet-managed Pods (use --ignore-daemonsets to ignore): kube-system/calico-node-7p9zx, kube-system/fluent-bit-x4c2m
DaemonSet-pods zijn ontworpen om er één per node te draaien. De DaemonSet-controller tolereert de node.kubernetes.io/unschedulable:NoSchedule taint, dus zelfs als drain ze toch zou evicten, maakt de controller ze meteen weer aan. Drain wil niet in die loop terechtkomen en vraagt je expliciet om ze over te slaan.
Geef --ignore-daemonsets mee bij elke drain. In een echt cluster heb je vrijwel altijd minstens een CNI-plugin (Calico, Cilium, Flannel) als DaemonSet, dus de flag is feitelijk niet optioneel.
kubectl drain worker-3 --ignore-daemonsets
De DaemonSet-pods blijven gewoon draaien op de gecordonde node tijdens het onderhoud. Als je de node verwijdert of uncordont, ruimt de DaemonSet-controller op. Als het onderhoud de kubelet herstart, worden de DaemonSet-pods door de kubelet opnieuw gestart bij boot.
Veelvoorkomende blokkade: emptyDir-pods blokkeren drain (--delete-emptydir-data)
De tweede keer dat je drain draait, krijg je dit:
error: unable to drain node "worker-3" due to error: cannot delete Pods with local storage (use --delete-emptydir-data to override): default/build-cache-pj8vn
emptyDir-volumes zijn pod-scoped scratchruimte op het filesystem van de node. Als de pod stopt, gaat de emptyDir met hem mee. Drain weigert standaard omdat er een reëel risico is dat je niet door hebt dat de pod data heeft staan. Buildcaches, sidecar-buffers en tmpfs-werkmappen leven allemaal in emptyDir. De pod evicten betekent die data weggooien.
Heb je de workload doorgelopen en is de data veilig om te verliezen (en dat is in verreweg de meeste emptyDir-cases zo), dan geef je de flag mee:
kubectl drain worker-3 --ignore-daemonsets --delete-emptydir-data
Een nameplate-detail. Deze flag heette tot Kubernetes 1.20 --delete-local-data. Pull request #95076 hernoemde hem naar --delete-emptydir-data, omdat de oude naam suggereerde dat de flag ook hostPath en persistent volumes raakte (dat doet hij niet; alleen emptyDir). Oude runbooks en Stack Overflow-antwoorden gebruiken nog vaak de oude naam. De compatibility shim is inmiddels eruit gehaald; op elk cluster dat je vandaag draait is --delete-emptydir-data de juiste schrijfwijze.
Als een workload écht emptyDir-data over onderhoud heen wil houden, klopt het ontwerp niet: emptyDir is per definitie ephemeral. Verplaats het naar een PersistentVolumeClaim of een hostPath-mount die met de node meeleeft.
Veelvoorkomende blokkade: pods zonder controller blokkeren drain (--force)
error: unable to drain node "worker-3" due to error: cannot delete Pods that declare no controller (use --force to override): default/debug-shell
Een pod die je rechtstreeks hebt aangemaakt (kubectl run, kubectl apply -f pod.yaml) zonder een Deployment, StatefulSet, ReplicaSet, Job of DaemonSet eromheen, is een "wees" pod. Niemand maakt hem opnieuw aan zodra hij weg is. Drain weigert standaard omdat hem evicten neerkomt op hem permanent kwijtraken, en de meeste operators willen dat eigenlijk niet.
De legitieme cases waarin je dat wel wil:
- Debugpods die je met
kubectl runhebt opgespawned voor diagnostiek. - Static pods die per ongeluk zijn blijven hangen na handmatige experimenten.
- Pods waarvan je de controller bewust hebt verwijderd (orphan-deletion sequences).
In die gevallen geef je --force mee:
kubectl drain worker-3 --ignore-daemonsets --delete-emptydir-data --force
Is de orphan-pod iets dat je echt nodig hebt, dan is de juiste fix om er eerst een Deployment van te maken, niet om --force toe te voegen. Drain vertelt je precies wat er kwetsbaar is in je cluster.
Veelvoorkomende blokkade: PDB blokkeert eviction (--disable-eviction als laatste redmiddel)
Wanneer een PodDisruptionBudget de eviction niet toestaat, geeft de Eviction API HTTP 429 (Too Many Requests). kubectl drain blijft retryen:
evicting pod default/web-api-7c5fbf6dd5-kf9lk
error when evicting pods/"web-api-7c5fbf6dd5-kf9lk" -n "default" (will retry after 5s):
Cannot evict pod as it would violate the pod's disruption budget.
Dit is precies hoe het systeem hoort te werken. De PDB zegt tegen drain: "wacht, deze pod nu evicten zou de applicatie onder minAvailable brengen". Het juiste antwoord is wachten. De Deployment-controller start vervangende pods op andere nodes, de currentHealthy van de PDB klimt, en de volgende retry slaagt wel.
Een PDB-geblokkeerde drain wordt pas een echt probleem als:
- De Deployment maar één replica heeft met een
minAvailable: 1PDB. Eviction kan dan nooit slagen; de applicatie is fundamenteel niet onderhoudsproof. - Een pod in
CrashLoopBackOffzit en de PDB hem nog steeds meetelt. (Kubernetes 1.26 introduceerdeunhealthyPodEvictionPolicy: AlwaysAllowprecies hiervoor; zet hem aan per PDB.) - Twee PDBs dezelfde pod selecteren, wat een HTTP 500 oplevert in plaats van een 429.
- Cluster autoscaling niet draait, dus er geen vervangende nodes zijn waar de geëvicte pods naartoe kunnen.
De ontsnappingsroute, als laatste redmiddel:
kubectl drain worker-3 \
--ignore-daemonsets \
--delete-emptydir-data \
--disable-eviction
--disable-eviction laat drain de Eviction API overslaan en direct DELETE op elke pod aanroepen. PDBs worden niet gecheckt. Dit is een gecontroleerde uitval: alle replica's die deze drain raakt op deze node verdwijnen tegelijk. Gebruik dit alleen als:
- Je de PDB-blokkade al hebt gediagnosticeerd.
- Je een onderhoudsmoment hebt waarin de applicatie de uitval kan hebben.
- Je expliciete businessgoedkeuring hebt voor de disruption.
Grijp je vaker naar --disable-eviction, dan zit het echte probleem dieper: workloads zijn niet ingericht voor veilig onderhoud. Fix de PodDisruptionBudgets en replica-aantallen; ga ze niet structureel omzeilen.
Drain-voortgang volgen
kubectl drain is praatziek en print één regel per pod. Op een node met 50 pods wordt dat onleesbaar. Handige patronen:
De podlijst zien krimpen in een tweede terminal:
watch -n 2 'kubectl get pods --field-selector spec.nodeName=worker-3 -A --no-headers | wc -l'
Eviction-events live volgen:
kubectl get events --watch --field-selector reason=Evicted
PDBs op nul allowed disruptions opsporen (als drain hangt):
kubectl get pdb --all-namespaces -o wide | awk '$5==0'
Doet kubectl drain minutenlang niks zichtbaars, dan is dat vrijwel altijd de oorzaak: een PDB op nul disruptions. Lokaliseer hem, beslis of je wacht op vervangende pods, of pak het noodluik.
Cloud-managed nodes: hoe nodepool-upgrades verschillen (GKE, EKS, AKS)
Als een managed Kubernetes-service een nodepool upgrade rolt, draait dezelfde cordon-drain-routine onder water, maar met vendorspecifieke timeouts en gedrag. Die getallen kennen voorkomt verrassingen tijdens upgrades.
| Service | Default drain-timeout | Aanpasbaar | Gedrag bij timeout |
|---|---|---|---|
| GKE (Google) | 1 uur | Nee | Forceert eviction van resterende pods, upgrade gaat door |
| EKS (AWS) | 15 minuten | Nee | PodEvictionFailure, upgrade faalt tenzij --force is meegegeven |
| AKS (Azure) | 30 minuten | Ja (--drain-timeout) |
Configureerbaar gedrag: standaard faalt de upgrade, Cordon-modus quarantaint de node |
Wat dit betekent voor cluster-operators:
- GKE laat een upgrade nooit falen door een vastlopende PDB. Na een uur was je
minAvailableeen suggestie. Hang je echt aan PDB-handhaving, dan is die verrassing reëel. - EKS is het strengst. Een PDB die kort op nul allowed disruptions staat is genoeg om een managed-nodegroup-upgrade te laten falen. Je geeft
--forcemee (de AWS-equivalent van--disable-eviction) of je fixt de PDB. - AKS is het flexibelst: je kunt de drain-timeout naar meerdere uren zetten, en de
--undrainable-node-behavior Cordonoptie houdt de upgrade in beweging door vastlopende nodes voor handmatige afhandeling te quarantainen.
Voor alle drie geldt: scan vóór een upgrade naar PDBs die gaan blokkeren:
kubectl get pdb --all-namespaces -o wide | awk 'NR==1 || $5==0'
Elke regel met ALLOWED DISRUPTIONS = 0 is een zo goed als zekere upgrade-failure op EKS, een uur vertraagde upgrade op GKE, en een quarantaine-event op AKS.
Het complete drain-commando (zoals je hem kopieert)
Het drain-commando dat in verreweg de meeste productie-onderhoudsmomenten geschikt is, op één plek:
kubectl drain worker-3 \
--ignore-daemonsets \
--delete-emptydir-data \
--grace-period=-1 \
--timeout=15m
Per flag de reden:
--ignore-daemonsets: elk cluster heeft CNI- en monitoring-DaemonSets; zonder deze flag start drain niet.--delete-emptydir-data: elke pod met een buildcache of scratchvolume blokkeert drain anders; je moet bewust beslissen dat ephemere data verloren mag gaan.--grace-period=-1: respecteer de eigenterminationGracePeriodSecondsvan elke pod. Een positieve waarde overschrijft de pod-grace-period, wat schone afsluitingen kan afkappen.-1(de default) is bijna altijd goed.--timeout=15m: geeft trage pods de ruimte om netjes te stoppen, maar voorkomt dat drain eindeloos blijft hangen op een verkeerd geconfigureerde PDB. Schroef hem op of af op basis van hoe lang je workloads doorgaans nodig hebben om te terminen.
Voeg --force toe als (en alleen als) je weet dat er ongecontroleerde pods op de node staan en je accepteert dat ze verloren gaan. Voeg --disable-eviction toe als (en alleen als) PDBs blokkeren en je goedkeuring hebt voor de uitval.
Voor een zero-downtime rollende upgrade van een hele nodepool: drain één node tegelijk, wacht tot de geëvicte pods elders weer Ready zijn, en pak dan de volgende.
Wanneer je escaleert
Drain hangt voorbij je timeout en je weet niet waarom. Verzamel dit voor je hulp inroept:
kubectl version(client en server)kubectl get nodes -o wide(zodat de node-statussen zichtbaar zijn)kubectl get pods --field-selector spec.nodeName=<node> -A -o wide(wat staat er nog op de node)kubectl get pdb --all-namespaces -o wide(PDB-status door het cluster)kubectl describe pdb <pdb>voor elke PDB op nul disruptions, inclusief events- Het exacte
kubectl drain-commando en de gebruikte flags - Cloudprovider en managed-service-tier (GKE, EKS, AKS, self-managed)
- Of de node bereikbaar is vanaf de control plane (
kubectl describe node <name>voor de laatste heartbeat) - Of het cluster genoeg vrije capaciteit heeft voor de geëvicte pods (
kubectl describe nodes | grep -A 5 Allocated)
Staat de node zelf op NotReady, dan is drain niet de juiste tool; de node herstellen of vervangen wel.
Hoe je het voorkomt in de toekomst
- Zet een PodDisruptionBudget op elke Deployment en StatefulSet. Draai nooit een productie-workload met één replica achter een PDB; of je draait twee replica's of je accepteert de uitval.
- Zet
unhealthyPodEvictionPolicy: AlwaysAllowop PDBs voor stateless services, zodat eenCrashLoopBackOffdrain niet eeuwig laat hangen. - Zet realistische
terminationGracePeriodSecondsop je pods. De default van 30 seconden volstaat voor stateless HTTP-services; databases, queueworkers en langlopende jobs hebben meer nodig. - Draai drains als onderdeel van je gewone onderhoudsritme, niet alleen tijdens incidents. De eerste drain van een onbekende workload moet niet om 03:00 zijn tijdens een kernel-CVE-respons.
- Audit voor autoscaling-clusters je
maxUnavailableenmaxSurgetegen je PDBs. De combinatie van "PDB staat 1 disruption toe" en "nodepool surget 5 nodes tegelijk" leidt tot voorspelbare upgrade-hangs. - Monitor
kube_poddisruptionbudget_status_current_healthyversuskube_poddisruptionbudget_status_desired_healthyuit kube-state-metrics, en alarmeer wanneer een PDB langer dan een paar minuten op nul disruptions staat.