Scheduling: selectors, affinity, taints and spread
How the scheduler filters and scores nodes, and every lever that steers a Pod: nodeName, nodeSelector, node affinity, pod affinity and anti-affinity, taints and tolerations, and topology spread.
After this lesson
- 01You can read a FailedScheduling event and name the constraint that rejected each node.
- 02You can pin Pods to labeled nodes with nodeSelector or node affinity, and keep replicas apart with pod anti-affinity or topology spread.
- 03You can taint a node, write the matching toleration, and explain why a toleration alone never attracts a Pod.
- 04You can place a Pod by hand with nodeName when the scheduler is not running.
This is the scheduling half of Configure Pod admission and scheduling (limits, node affinity, etc.). Scheduling tasks have a satisfying property: the grader can see exactly where your Pods landed. That cuts both ways. A Deployment that is Running on the wrong node scores the same as one that is Pending, so always finish with kubectl get pods -o wide.
How the scheduler chooses a node
kube-scheduler watches for Pods with an empty spec.nodeName. It first filters out nodes that cannot run the Pod: not enough unrequested CPU or memory, a nodeSelector or required affinity that does not match, a taint the Pod does not tolerate, a host port already taken, a volume in another zone. It then scores the survivors (spreading, preferred affinities, balanced allocation, images already present) and binds the Pod to the best one by setting nodeName. If no node survives the filter, the Pod stays Pending with a FailedScheduling event such as 0/3 nodes are available: 1 node(s) had untolerated taint(s), 2 node(s) didn't match pod anti-affinity rules. Read it with kubectl describe pod or kubectl get events --field-selector reason=FailedScheduling; it names one reason per rejected node.
nodeSelector, node affinity and nodeName
spec:
nodeSelector: # every label must match (AND)
pool: frontend
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms: # terms are ORed ...
- matchExpressions: # ... expressions inside one term are ANDed
- key: kubernetes.io/arch
operator: In # In, NotIn, Exists, DoesNotExist, Gt, Lt
values: ["amd64", "arm64"]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80 # 1-100, added to the node's score
preference:
matchExpressions:
- key: disktype
operator: In
values: ["ssd"]When both nodeSelector and nodeAffinity are present, a node must satisfy both. IgnoredDuringExecution means a Pod already running stays put if the node's labels change later. NotIn and DoesNotExist give you anti-affinity to nodes, which is often simpler than tainting them.
spec.nodeName skips the scheduler entirely: the kubelet on that node just starts the Pod. No scheduler means no resource filtering (the kubelet rejects a Pod that does not fit, with a reason like OutOfcpu) and no NoSchedule check, because the kubelet only enforces NoExecute taints. That makes it the tool for manual scheduling when kube-scheduler is down and Pods sit Pending. You cannot patch nodeName onto an existing Pod; the API rejects the update. Delete the Pod and create it again with nodeName set, then go fix the scheduler (on kubeadm clusters, its static Pod manifest in /etc/kubernetes/manifests).
Pod affinity, anti-affinity and topology spread
template:
metadata:
labels:
app: api
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: api
topologyKey: kubernetes.io/hostname # "same place" means same node
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 50
podAffinityTerm:
labelSelector:
matchLabels:
app: cache
topologyKey: topology.kubernetes.io/zone # near the cache, same zone
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule # or ScheduleAnyway (a preference)
labelSelector:
matchLabels:
app: api
containers:
- name: api
image: nginx:1.27Pod affinity rules match other Pods' labels, within the Pod's own namespace unless you add namespaces or namespaceSelector. The topologyKey is a node label that defines a domain: per node with kubernetes.io/hostname, per zone with topology.kubernetes.io/zone. Topology spread is the gentler tool: maxSkew: 1 lets domains differ by at most one Pod, so it still schedules five replicas on two nodes, where a required anti-affinity would leave three Pending.
Taints and tolerations
kubectl label node cka-worker pool=frontend
kubectl get nodes -L pool # show one label as a column
kubectl label node cka-worker pool- # remove it
kubectl taint node cka-worker2 dedicated=batch:NoSchedule
kubectl get node cka-worker2 -o jsonpath='{.spec.taints}'; echo
kubectl taint node cka-worker2 dedicated=batch:NoSchedule- # remove it (trailing minus)
kubectl describe node cka-control-plane | grep Taints # node-role.kubernetes.io/control-plane:NoSchedule tolerations:
- key: dedicated # matches dedicated=batch:NoSchedule exactly
operator: Equal
value: batch
effect: NoSchedule
- key: node-role.kubernetes.io/control-plane
operator: Exists # any value; no value field allowed
effect: NoSchedule
- key: node.kubernetes.io/unreachable
operator: Exists
effect: NoExecute
tolerationSeconds: 60 # stay 60s on an unreachable node, then be evictedA taint repels; a toleration only permits. A Pod that tolerates dedicated=batch can still land on any untainted node, so dedicating nodes takes both halves: a taint to keep others off and a label plus nodeSelector or affinity to pull the right Pods on. Every Pod also gets not-ready and unreachable NoExecute tolerations of 300 seconds from the DefaultTolerationSeconds admission plugin, which is why Pods on a dead node take about five minutes to be replaced. On this shared lab, never use NoExecute: it evicts every Pod on the node that lacks a toleration, other lessons' Pods included.
In the docs · the tab you get on exam day
- Assigning Pods to Nodes ↗search: “assign pods to nodes”
- Taints and Tolerations ↗search: “taints tolerations”
- Pod Topology Spread Constraints ↗search: “topology spread constraints”
- Assign Pods to Nodes using Node Affinity ↗search: “node affinity”
context · kubectl config use-context kind-cka
Pin a Deployment to a labeled node
pool=frontend to node cka-worker. Then in namespace wl-sched create a Deployment frontend with 3 replicas of nginx:1.27 whose Pods may only be scheduled onto nodes carrying that label. All 3 Pods must be running.- 01
Set up the scenario
curl -fsSL https://codewithmuh.com/labs/cka/t/wl-sched-1/setup | bash - 02
Solve it in your terminal
Work the way the exam makes you: kubectl, the docs, nothing else. Put any answer files under
/tmp/cka/. - 03
Check your work
curl -fsSL https://codewithmuh.com/labs/cka/t/wl-sched-1/verify | bashCounts toward your readiness score. - 04
Clean up
curl -fsSL https://codewithmuh.com/labs/cka/t/wl-sched-1/cleanup | bash
context · kubectl config use-context kind-cka
Taint a node and run a Pod that tolerates it
cka-worker2 with key dedicated, value batch and effect NoSchedule. Then in namespace wl-sched create a Pod batch-runner (image busybox:1.36, command sleep 3600) that tolerates this taint and runs on cka-worker2.- 01
Set up the scenario
curl -fsSL https://codewithmuh.com/labs/cka/t/wl-sched-2/setup | bash - 02
Solve it in your terminal
Work the way the exam makes you: kubectl, the docs, nothing else. Put any answer files under
/tmp/cka/. - 03
Check your work
curl -fsSL https://codewithmuh.com/labs/cka/t/wl-sched-2/verify | bashCounts toward your readiness score. - 04
Clean up
curl -fsSL https://codewithmuh.com/labs/cka/t/wl-sched-2/cleanup | bash
context · kubectl config use-context kind-cka
Spread replicas across nodes with anti-affinity
wl-sched create a Deployment spread with 2 replicas of nginx:1.27, Pod label app=spread. Add a required pod anti-affinity rule so that two Pods of this Deployment can never run on the same node. Both Pods must be running.- 01
Set up the scenario
curl -fsSL https://codewithmuh.com/labs/cka/t/wl-sched-3/setup | bash - 02
Solve it in your terminal
Work the way the exam makes you: kubectl, the docs, nothing else. Put any answer files under
/tmp/cka/. - 03
Check your work
curl -fsSL https://codewithmuh.com/labs/cka/t/wl-sched-3/verify | bashCounts toward your readiness score. - 04
Clean up
curl -fsSL https://codewithmuh.com/labs/cka/t/wl-sched-3/cleanup | bash
Finished reading?
0 of 3 tasks passed. Readiness counts tasks, so run them before you move on.