Node Drain, Upgrade & Recovery
Take a node out of service without taking the application with it, and find out which workloads were never ready for it.
- Time
- 55 min
- Level
- Advanced
- Objectives
- 4 objectives
- Cost
- Free
Where this fits in the platform
Already built
This lab adds
- A node taken out of service with the application still up
Which lets you
—
Before you start
You will need
- kind (multi-node)
- kubectl 1.28+
You do not need these already — the lab environment below provides them.
You will be able to
- Cordon and drain a node safely
- Protect availability during voluntary disruption with a PDB
- Recognise workloads that cannot survive rescheduling
Cost — Free
— a multi-node kind cluster. See the setup note; a single node cannot demonstrate rescheduling.
Nothing to pay in the browser. Open the terminal runs this against a simulated cloud — the same API calls and the same commands, with no account and no bill. The figure above applies only if you build it in your own.
The scenario#
The cluster needs a Kubernetes upgrade. That means taking each node out of service in turn, and the first one you try teaches you which of your workloads were only ever running by luck.
This evicts running workloads. Use a throwaway cluster.
Hands-on environment
Run this lab in a real terminal, free and in your browser. The environment is temporary and yours alone — break it as much as you like.
Open the terminalOpens in Killercoda, in a new tab — keep this page open for the steps.
Run it on your own machine
Run this lab on your own machine. One command starts the environment, with everything the lab needs already installed:
You will need:
- docker
- kubectl
- kind
git clone https://github.com/EgyKode/EgyKode-lab.git
cd EgyKode-lab
./egykode start k8s
./egykode shellYou need Docker and Git installed. Everything else runs inside the environment. The first start downloads it and takes a few minutes; later starts are seconds.
Not sure what you already have? Run: npm run doctor — it checks and changes nothing.
Anything you tick here is your own record. EgyKode cannot see inside that terminal, so the success criteria stay self-assessed even when the environment checks your work for you.
Something to keep alive
Step 1 of 6
Setup: more than one node#
cat <<'EOF' | kind create cluster --name ops --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
EOF
kubectl get nodesA single-node cluster cannot demonstrate any of this — there is nowhere to
reschedule to, and every Pod simply goes Pending.
What you are proving: You can deploy a workload whose availability you can measure while the cluster changes underneath it
This step settles no success criterion on its own.
kubectl create deployment web --image=nginx:1.27-alpine --replicas=4
kubectl expose deployment web --port=80
kubectl get pods -o wide # note the spread across nodesSteady traffic, in another terminal:
kubectl run load --rm -it --image=curlimages/curl --restart=Never -- \
sh -c 'while true; do curl -s -o /dev/null -w "%{http_code} " http://web; sleep 0.2; done'What you are proving: You can drain a node without the application dropping a request
Marking this settles success criterion 1.
kubectl cordon ops-worker
kubectl get nodes # SchedulingDisabled — nothing new lands herecordon stops new Pods arriving. Existing ones keep running, which makes it
safe to run well before the maintenance window.
kubectl drain ops-worker --ignore-daemonsets --delete-emptydir-data--ignore-daemonsets— DaemonSet Pods are recreated on the same node by design, so a drain can never evict them and refuses to start without this.--delete-emptydir-data— acknowledges thatemptyDirdata on this node is destroyed. Say it deliberately.
Watch the traffic terminal. Count non-200 responses.
What you are proving: You can identify a workload that cannot reschedule cleanly, and say why
Marking this settles success criterion 3.
The Pod stayed in the Service's endpoints while it was terminating. The fix is in the workload, not in the drain:
terminationGracePeriodSeconds: 30
containers:
- name: web
readinessProbe:
httpGet: { path: /, port: 80 }
periodSeconds: 3
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5"]The preStop sleep is not superstition. On termination, two things happen
concurrently: the Pod is removed from endpoints, and SIGTERM is sent. The
five seconds let the endpoint removal propagate to every kube-proxy before
the process starts shutting down, so no traffic is routed to a dying Pod.
What you are proving: You can write a PodDisruptionBudget that blocks a drain which would breach availability
Marking this settles success criterion 2.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web
spec:
minAvailable: 3
selector:
matchLabels:
app: webkubectl apply -f pdb.yaml
kubectl drain ops-worker2 --ignore-daemonsets 2>&1 | head -3
# Cannot evict pod as it would violate the disruption budgetA PDB governs voluntary disruption only — drains, upgrades, autoscaler scale-downs. It does nothing about a crash or a node failure, and that distinction is the whole point: it prevents self-inflicted outages during maintenance.
minAvailable must be lower than the replica count, or no drain can ever
proceed.
What you are proving: You can return a node to service and confirm it receives Pods again
Marking this settles success criterion 4.
kubectl uncordon ops-worker
kubectl get nodes
kubectl rollout restart deployment/web # rebalance, once it is schedulableNote that nothing moves back on its own. Kubernetes does not rebalance running Pods, so after a drain the remaining nodes stay loaded until something forces a reschedule.
What you are proving: You can describe what a real version upgrade adds beyond the drain itself
This step settles no success criterion on its own.
cordon → drain → upgrade kubelet/kubeadm → uncordon → verify → next node- Upgrade the control plane first, one node at a time.
- Never skip a minor version — 1.28 → 1.30 is two upgrades.
- Check API deprecations before starting: a removed API version breaks workloads on the new nodes only, so it looks like a partial outage.
kubectl get --raw /metrics | grep apiserver_requested_deprecated_apisThat metric names what is still calling a deprecated API, which is exactly the list you want before an upgrade rather than after.
drain hangs indefinitely
Something cannot be evicted: a bare Pod with no controller, or a PDB that cannot be satisfied. The message names it.
Requests fail during the drain
The Pod left the process running while still in endpoints. Add a readiness probe and a preStop delay.
Everything goes Pending
Nowhere to reschedule — a single-node cluster, or the remaining nodes lack capacity.
Pods do not return after uncordon
Expected. Kubernetes does not rebalance running Pods; force it with kubectl rollout restart.
Clean up#
Run this even if you did not finish.
Destructive — This removes real resources. Check which environment you are in first.
kubectl uncordon $(kubectl get nodes -o name)
kubectl delete deployment web --ignore-not-found
kind delete cluster --name opsCost of this lab: Free — a multi-node kind cluster. See the setup note; a single node cannot demonstrate rescheduling.
Success criteria
0 of 4
The concept behind it
Next up
Lab 58 of 59 on the project path