Kubernetes Node Management — Cordon, Drain, Upgrades
Learn Kubernetes Node Management — Cordon, Drain, Upgrades with plain-English explanations and real examples..
20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.
- ✓kubectl v1.28+, Kubernetes cluster v1.28+, jq (optional), ssh access to nodes, basic understanding of Kubernetes pods and deployments
Think of Kubernetes Node Management — Cordon, Drain, Upgrades like a tool in your DevOps toolkit — once you understand what it does and when to reach for it, managing Kubernetes clusters becomes second nature.
Welcome to Kubernetes Node Management — Cordon, Drain, Upgrades. We'll break this down from first principles.
Understanding Node States: Ready, NotReady, and SchedulingDisabled
A Kubernetes node can be in several states that affect workload scheduling. The Ready status indicates the node is healthy and can accept pods. NotReady means the node is unreachable or unhealthy — pods may be evicted after a timeout. SchedulingDisabled is a manual state set by cordon that prevents new pods from being scheduled while existing pods continue running. Understanding these states is critical for planning maintenance. When you cordon a node, its spec.unschedulable field is set to true, which the scheduler respects. The node remains Ready but won't accept new pods. This is the first step in a safe drain sequence.
SchedulingDisabled is not a condition but a field. Use kubectl describe node to see all conditions.Ready but have degraded performance. Always check resource pressure conditions before draining.unschedulable without affecting existing pods.Cordon: The Safety Gate Before Maintenance
The cordon command marks a node as unschedulable. It's the first step in any node maintenance procedure because it prevents new pods from landing on the node while existing workloads continue. This is crucial for zero-downtime operations. Without cordoning, new pods could be scheduled during the drain, causing race conditions. Cordon is idempotent — running it multiple times has no side effect. Use it before any planned maintenance, even if you're not draining immediately. It gives you a safety window to verify the node's health and plan the drain. The command updates the node's spec.unschedulable field to true.
kubectl drain do this automatically, but explicit cordon gives you a checkpoint.Drain: Evicting Pods Gracefully
The drain command evicts all pods from a node while respecting PodDisruptionBudgets (PDBs) and terminationGracePeriodSeconds. It first cordons the node (if not already), then evicts pods one by one. DaemonSet pods are not evicted by default — you must use --ignore-daemonsets or --delete-emptydir-data flags. Drain waits for PDBs to allow at least one replica to remain available. If a PDB blocks eviction, drain will hang until timeout. This is intentional to protect application availability. Use --force to bypass PDBs (dangerous in production). Drain also respects pod priority — higher priority pods are evicted last.
--disable-eviction to force delete pods (not recommended).Uncordon: Returning the Node to Service
After maintenance, uncordon marks the node as schedulable again. It sets spec.unschedulable to false. Pods are not automatically rescheduled onto the node — they will be scheduled only when new pods are created or when existing pods are rescheduled due to other reasons. This is a common misconception. If you want to rebalance workloads, you may need to manually delete pods on other nodes or use descheduler. Uncordon is safe to run at any time, even if the node is not fully ready. However, ensure the node's kubelet is healthy and all system components are running before uncordoning.
kubectl get nodes and check that the node's Ready status is True.Ready but had a broken CNI plugin, causing new pods to fail. Always validate node health before uncordoning.Drain Flags and Options You Must Know
The kubectl drain command has several flags that control behavior. --ignore-daemonsets is almost always needed because DaemonSet pods are not evicted by default. --delete-emptydir-data allows eviction of pods using emptyDir volumes — without it, drain will refuse to evict such pods. --force bypasses PDBs and termination grace periods — use only as a last resort. --grace-period sets the duration for pod termination. --timeout specifies how long to wait before giving up. --pod-selector allows draining only pods matching a label. Understanding these flags prevents unexpected failures. For example, forgetting --ignore-daemonsets will cause drain to hang indefinitely.
--force can cause abrupt pod termination, leading to data corruption or service disruption. Only use when node is already down and pods must be removed.--ignore-daemonsets on a node with 50 DaemonSet pods. The command hung for 30 minutes until timeout. We now enforce a script that includes required flags.--ignore-daemonsets and --delete-emptydir-data in production.PodDisruptionBudget: The Gatekeeper of Availability
PodDisruptionBudget (PDB) is a Kubernetes resource that limits the number of pods that can be voluntarily disrupted at a time. It ensures that a minimum number of pods remain available during voluntary disruptions like node drains. PDBs are defined per application and specify minAvailable or maxUnavailable. When draining a node, the eviction API checks PDBs. If evicting a pod would violate the PDB, the eviction is rejected. This can cause drain to hang. To avoid this, ensure your PDBs are configured with enough buffer. For example, if you have 3 replicas, set maxUnavailable: 1 to allow one pod to be disrupted at a time.
maxUnavailable: 1 for multi-replica deployments. For single-replica, consider using minAvailable: 0 during maintenance windows.minAvailable: 3 for a 3-replica deployment. During a rolling upgrade, drain could not evict any pod, stalling the entire cluster upgrade. We now set maxUnavailable: 1 for all stateless apps.Node Upgrades: A Step-by-Step Workflow
Upgrading a Kubernetes node (e.g., kubelet, container runtime, or OS) requires a careful workflow to avoid downtime. The standard process: 1) Cordon the node to prevent new pods. 2) Drain the node to evict existing pods. 3) Perform the upgrade (e.g., package update, reboot). 4) Verify node health (kubelet, node status). 5) Uncordon the node. This sequence ensures zero pod disruption. For control plane nodes, the process is similar but requires special attention to etcd and API server quorum. Always upgrade one node at a time, especially in production. Automate this with tools like kubeadm upgrade node or cluster-api.
kOps or AKS upgrade.Automating Node Management with Scripts and Tools
Manual node management doesn't scale. In production, automate cordon/drain/uncordon with scripts or tools like Ansible, Terraform, or cluster-api. A common pattern is to write a bash script that iterates over nodes, cordons, drains, performs maintenance, verifies, and uncordons. Use kubectl with --context for multi-cluster support. For cloud-managed clusters (EKS, AKS, GKE), use their node management features (e.g., AWS Node Termination Handler). For on-prem, consider using a tool like kured (Kubernetes Reboot Daemon) that automates node reboots based on reboot-required files.
Handling Edge Cases: When Drain Fails
Drain can fail for several reasons: PDB violations, pods with local storage (emptyDir without --delete-emptydir-data), unmanaged pods (not backed by a controller), or pods with strict inter-pod anti-affinity. When drain fails, it will output the reason. Common fixes: adjust PDBs, add --delete-emptydir-data, or delete unmanaged pods manually. For pods with anti-affinity, you may need to temporarily scale down the deployment. In extreme cases, use --force but be aware of the risks. Always investigate the root cause rather than blindly forcing. A failed drain is a signal that your cluster configuration needs attention.
Best Practices for Production Node Management
- Always cordon before drain, even if drain does it automatically. 2) Use PDBs with
maxUnavailable: 1for all stateless workloads. 3) SetterminationGracePeriodSecondsappropriately (e.g., 30s for web apps, 120s for batch jobs). 4) Use--ignore-daemonsetsand--delete-emptydir-datain all drain commands. 5) Test drain on a non-production cluster first. 6) Monitor cluster during maintenance with alerts on node status and pod evictions. 7) Have a rollback plan: if a node fails to come back after upgrade, have a process to replace it. 8) Use cluster autoscaler to handle capacity during node drains.
terminationGracePeriodSeconds to match your application's shutdown time. Too short causes abrupt kills; too long delays drains.terminationGracePeriodSeconds: 120 for a legacy app that needed to flush buffers. This prevented data loss during drains but increased drain time. Trade-offs are inevitable.Troubleshooting Common Node Management Issues
Common issues: Node stuck in NotReady after reboot — check kubelet logs (journalctl -u kubelet). Drain hangs — check PDBs and pod eviction events (kubectl get events --field-selector involvedObject.kind=Pod). Uncordon doesn't schedule pods — check node conditions and taints. Pods evicted but not rescheduled — check scheduler logs or node resources. Always use kubectl describe node to get a full picture. For persistent issues, consider replacing the node rather than debugging. In cloud environments, terminating and recreating the node is often faster.
Advanced: Using Descheduler for Workload Rebalancing
After uncordoning a node, pods don't automatically move to it. The descheduler is a Kubernetes add-on that evicts pods based on policies to rebalance workloads. For example, after a node returns from maintenance, the descheduler can evict pods from other nodes to fill the empty node. This is useful for optimizing resource utilization. However, descheduler must be used carefully — it can cause unnecessary pod churn. Configure it with policies like LowNodeUtilization and HighNodeUtilization. Always run descheduler in dry-run mode first. In production, we run descheduler every 15 minutes with a MaxNoOfPodsToEvictPerNode limit.
MaxNoOfPodsToEvictPerNode and test in staging first.| File | Command / Code | Purpose |
|---|---|---|
| check-node-status.sh | kubectl get nodes -o wide | Understanding Node States |
| cordon-node.sh | kubectl cordon node-1 | Cordon |
| drain-node.sh | kubectl drain node-1 --ignore-daemonsets --delete-emptydir-data --timeout=300s | Drain |
| uncordon-node.sh | kubectl uncordon node-1 | Uncordon |
| drain-with-flags.sh | kubectl drain node-1 \ | Drain Flags and Options You Must Know |
| pdb-example.yaml | apiVersion: policy/v1 | PodDisruptionBudget |
| node-upgrade-workflow.sh | kubectl cordon node-1 | Node Upgrades |
| automate-node-maintenance.sh | set -euo pipefail | Automating Node Management with Scripts and Tools |
| debug-drain-failure.sh | kubectl drain node-1 --ignore-daemonsets | Handling Edge Cases |
| deployment-with-pdb.yaml | apiVersion: apps/v1 | Best Practices for Production Node Management |
| troubleshoot-node.sh | kubectl describe node node-1 | grep -A5 Conditions | Troubleshooting Common Node Management Issues |
| descheduler-policy.yaml | apiVersion: "descheduler/v1alpha2" | Advanced |
Key takeaways
maxUnavailable: 1 for stateless workloads.--force unless necessary.Interview Questions on This Topic
What is the difference between cordon and drain?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.
That's Kubernetes. Mark it forged?
4 min read · try the examples if you haven't