Kubernetes node drain en cordon: veilig onderhoud zonder downtime

Veilig onderhoud op een Kubernetes-node loopt via twee commando's: kubectl cordon zorgt dat er geen nieuwe pods meer landen, en kubectl drain verwijdert de bestaande pods netjes via de Eviction API. Deze gids loopt door de volledige cordon-drain-onderhoud-uncordon flow, de flags die elke drain nodig heeft (--ignore-daemonsets, --delete-emptydir-data, --force, --disable-eviction), en hoe managed Kubernetes op AWS, Azure en GCP verschillen in drain-timeouts.

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

  • kubectl verbonden met een Kubernetes 1.27 of nieuwer cluster (de Eviction API en de policy/v1 PDB-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/eviction create) en nodes te patchen (nodes patch)
  • 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 Pending hangen 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 je kubectl uncordon. Om hem permanent te verwijderen is kubectl delete node een 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-daemonsets mee 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 run hebt 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: 1 PDB. Eviction kan dan nooit slagen; de applicatie is fundamenteel niet onderhoudsproof.
  • Een pod in CrashLoopBackOff zit en de PDB hem nog steeds meetelt. (Kubernetes 1.26 introduceerde unhealthyPodEvictionPolicy: AlwaysAllow precies 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:

  1. Je de PDB-blokkade al hebt gediagnosticeerd.
  2. Je een onderhoudsmoment hebt waarin de applicatie de uitval kan hebben.
  3. 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 minAvailable een 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 --force mee (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 Cordon optie 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 eigen terminationGracePeriodSeconds van 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: AlwaysAllow op PDBs voor stateless services, zodat een CrashLoopBackOff drain niet eeuwig laat hangen.
  • Zet realistische terminationGracePeriodSeconds op 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 maxUnavailable en maxSurge tegen 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_healthy versus kube_poddisruptionbudget_status_desired_healthy uit kube-state-metrics, en alarmeer wanneer een PDB langer dan een paar minuten op nul disruptions staat.

Terugkerende server- of deploymentproblemen?

Ik help teams productie betrouwbaar maken met CI/CD, Kubernetes en cloud—zodat fixes blijven en deploys geen stress meer zijn.

Bekijk DevOps consultancy

Doorzoek deze site

Begin met typen om te zoeken, of blader door de kennisbank en blog.