Every certification on the KubeAstronaut path — CKA, CKAD, and the conceptual KCNA — tests one skill that separates people who use Kubernetes from people who understand it: making the scheduler place a Pod exactly where you want it. The default scheduler already spreads Pods across healthy nodes with enough resources, but real clusters have constraints it cannot guess. GPU nodes should only run GPU workloads. A cache Pod should sit on the same node as the app that reads it. Three replicas of a database must never land on the same node, or a single node failure takes them all down.
Kubernetes gives you four mechanisms to express those constraints, and the exams love to test whether you can tell them apart: taints and tolerations, nodeSelector, node affinity, and pod (anti-)affinity — plus topology spread constraints as the modern balancing tool. This guide walks each one with working YAML and the exact kubectl commands you will need in a performance task, then finishes with a decision table and FAQ so that when a scenario appears, the right tool is already obvious. If you are still mapping out the broader path, start with the KubeAstronaut certification guide and the CKA workloads & scheduling guide.
The Two Directions of Pod Placement
The single mental model that unlocks this whole topic is direction. Every scheduling mechanism either repels Pods or attracts them, and it acts from the perspective of either the node or the Pod:
| Mechanism | Lives on | Direction | One-line job |
|---|---|---|---|
| Taint | Node | Repel | ”Keep Pods off me unless they tolerate this.” |
| Toleration | Pod | Permit | ”I can tolerate that taint.” |
| nodeSelector | Pod | Attract (hard) | “Only run me on nodes with this exact label.” |
| Node affinity | Pod | Attract (hard or soft) | “Prefer/require nodes matching this expression.” |
| Pod affinity | Pod | Attract (to other Pods) | “Place me near Pods matching this label.” |
| Pod anti-affinity | Pod | Repel (from other Pods) | “Keep me away from Pods matching this label.” |
| Topology spread | Pod | Balance | ”Spread my replicas evenly across zones/nodes.” |
Notice the asymmetry that the exam exploits: taints repel from the node side, while affinity attracts from the Pod side. They are not opposites of each other — they are complementary. A GPU node is usually tainted (so random Pods stay off it) and GPU Pods use node affinity (so they are drawn to it). You need both, and questions that mention “dedicated nodes” almost always want that pairing.
Taints and Tolerations: Repelling Pods From Nodes
A taint is applied to a node and marks it as unsuitable for Pods that do not explicitly tolerate it. A toleration is applied to a Pod and lets it schedule onto a node with a matching taint. Think of a taint as a “keep out” sign and a toleration as the matching key.
A taint has three parts — a key, an optional value, and an effect — written as key=value:effect. The effect is where exam questions concentrate, because the three effects behave very differently:
| Effect | What it does | Affects running Pods? |
|---|---|---|
NoSchedule | New Pods without a matching toleration are not scheduled here. | No — existing Pods stay. |
PreferNoSchedule | The scheduler tries to avoid placing intolerant Pods here, but will if it must. | No. |
NoExecute | Intolerant Pods are not scheduled and already-running intolerant Pods are evicted. | Yes — evicts. |
That last row is the classic trap. If a question says “existing Pods on the node should be evicted” or “immediately removed,” the answer is NoExecute. If it says “no new Pods should schedule but running Pods are unaffected,” it is NoSchedule.
Applying and removing a taint
# Taint a node so only tolerating Pods can schedule
kubectl taint nodes node01 gpu=true:NoSchedule
# Remove that taint (note the trailing minus sign)
kubectl taint nodes node01 gpu=true:NoSchedule-
# Inspect the taints on a node
kubectl describe node node01 | grep -i taints
Tolerating a taint in a Pod spec
A toleration must match the key, effect, and (if present) value. The operator field controls the match: Equal requires the value to match; Exists matches any value for that key.
apiVersion: v1
kind: Pod
metadata:
name: gpu-workload
spec:
tolerations:
- key: "gpu"
operator: "Equal"
value: "true"
effect: "NoSchedule"
containers:
- name: cuda
image: nvidia/cuda:12.0-base
Two facts the exams reward you for knowing. First, a toleration only permits scheduling — it does not force the Pod onto the tainted node. A tolerating Pod can still land on any other node with room. To pull it to the GPU node you also need node affinity or a nodeSelector. Second, control-plane nodes are tainted by default (for example node-role.kubernetes.io/control-plane:NoSchedule), which is exactly why your workloads do not normally run there — and why DaemonSets that must run everywhere, like a CNI or log agent, ship with broad tolerations.
nodeSelector: The Simplest Attraction
nodeSelector is the oldest and simplest way to constrain a Pod to a subset of nodes. You label the nodes, then name those labels in the Pod spec. It is a hard requirement: if no node carries every label listed, the Pod stays Pending.
# Label a node
kubectl label nodes node02 disktype=ssd
apiVersion: v1
kind: Pod
metadata:
name: fast-storage-app
spec:
nodeSelector:
disktype: ssd
containers:
- name: app
image: nginx
nodeSelector matches on equality only — a flat map of label: value pairs, all of which must be present. It cannot express “in this set of values,” “not equal to,” or “prefer but don’t require.” The moment a scenario needs any of that expressiveness, you have outgrown nodeSelector and the answer becomes node affinity.
Node Affinity: Expressive, Hard or Soft Attraction
Node affinity does everything nodeSelector does and more. It supports set-based operators (In, NotIn, Exists, DoesNotExist, Gt, Lt) and — crucially — two flavors of strictness whose long names you should be able to recognize on sight:
requiredDuringSchedulingIgnoredDuringExecution— a hard rule. The Pod will not schedule unless the rule is satisfied. This is the affinity equivalent ofnodeSelector.preferredDuringSchedulingIgnoredDuringExecution— a soft rule with aweight(1–100). The scheduler tries to honor it, but will place the Pod elsewhere rather than leave itPending.
The shared suffix, IgnoredDuringExecution, tells you these rules are evaluated only at scheduling time. If a node’s labels change later, an already-running Pod is not evicted. (A RequiredDuringExecution variant that would evict is planned but not yet part of the stable behavior you are tested on.)
apiVersion: v1
kind: Pod
metadata:
name: zone-pinned-app
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- us-east-1a
- us-east-1b
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 50
preference:
matchExpressions:
- key: disktype
operator: In
values:
- ssd
containers:
- name: app
image: nginx
Read that spec the way an exam wants you to: the Pod must land in zone us-east-1a or us-east-1b (hard), and among qualifying nodes it prefers SSD nodes (soft). If every SSD node is full, it still schedules on a non-SSD node in one of the two zones. Only if no node in either zone has room does it stay Pending.
nodeSelector vs node affinity — the one-liner
Use nodeSelector for a single, simple equality constraint. Use node affinity when you need set-based matching (In/NotIn), a soft preference, or multiple weighted preferences. They can coexist, and both are ANDed together if you specify both.
Pod Affinity and Anti-Affinity: Placing Pods Relative to Other Pods
Node affinity attracts a Pod to nodes by their labels. Pod affinity and anti-affinity place a Pod relative to other Pods by their labels — “run me near Pods that look like X” or “keep me away from Pods that look like X.” This is how you co-locate a web tier with its cache, or guarantee that replicas of the same app never share a node.
The concept that trips people up is topologyKey. Pod (anti-)affinity does not operate on individual nodes; it operates on topology domains defined by a node label. The most common values are:
kubernetes.io/hostname— the domain is a single node.topology.kubernetes.io/zone— the domain is an availability zone.
The rule reads: “relative to the domain identified by topologyKey, be near (affinity) or away from (anti-affinity) Pods matching this label selector.”
Anti-affinity: spread replicas across nodes
A textbook high-availability requirement — no two replicas of the same Deployment on the same node — is pure anti-affinity with topologyKey: kubernetes.io/hostname:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: web
topologyKey: kubernetes.io/hostname
containers:
- name: web
image: nginx
Because the anti-affinity is required, each web Pod refuses to schedule onto a node that already runs a web Pod. With three replicas and only two nodes, the third Pod stays Pending — a deliberate trade of availability over density that the exam may ask you to explain.
Affinity: co-locate with a dependency
Flip podAntiAffinity to podAffinity and the Pod is drawn toward matching Pods instead. A common pattern places a cache next to the app it serves so requests stay on-node:
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: web
topologyKey: kubernetes.io/hostname
A practical warning worth remembering for both the exam and production: required pod anti-affinity is computationally expensive and can leave Pods Pending in large clusters. For simple “spread evenly” goals, the modern answer is topology spread constraints.
Topology Spread Constraints: Even Distribution the Easy Way
Topology spread constraints let you say “keep my Pods evenly balanced across a topology” without the sharp edges of required anti-affinity. The core field is maxSkew — the maximum allowed difference between the most and least populated domain.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 6
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
containers:
- name: web
image: nginx
With maxSkew: 1 across three zones, six replicas distribute two per zone. whenUnsatisfiable mirrors the hard/soft split you already learned: DoNotSchedule is strict (leave the Pod Pending rather than break balance), while ScheduleAnyway is soft (place it and tolerate imbalance). If you understand required vs preferred affinity, you already understand this field.
Bringing It Together: Dedicated GPU Nodes
The most instructive scenario combines everything, because it is the one real clusters actually implement. You have GPU nodes and you want only GPU workloads on them, and you want those workloads to actually land there. One mechanism is not enough:
- Taint the GPU nodes so ordinary Pods are repelled:
kubectl taint nodes gpu01 gpu=true:NoSchedule. Label them too:kubectl label nodes gpu01 hardware=gpu. - Add a toleration to GPU Pods so they are allowed onto the tainted nodes.
- Add node affinity (or a nodeSelector) so GPU Pods are pulled onto the
hardware=gpunodes rather than scheduling elsewhere.
Taint alone keeps others off but does not attract GPU Pods. Affinity alone attracts GPU Pods but does not keep others off. The exam’s favorite “dedicated nodes” question is really checking whether you know you need both directions at once.
Decision Table: Which Mechanism Does the Scenario Want?
| The scenario says… | Reach for… |
|---|---|
| ”Reserve these nodes; keep other Pods off.” | Taint (NoSchedule) on the nodes. |
| ”Evict Pods that don’t belong here now.” | Taint with NoExecute. |
| ”Let this Pod run on the reserved/tainted nodes.” | Toleration on the Pod. |
| ”Run only on nodes with label X=Y (simple).” | nodeSelector. |
| ”Require nodes in a set of zones, prefer SSD.” | Node affinity (required + preferred). |
| ”Co-locate this Pod with app=web.” | Pod affinity. |
| ”Never put two replicas on the same node.” | Pod anti-affinity, topologyKey: hostname. |
| ”Spread replicas evenly across zones.” | Topology spread constraints (maxSkew). |
| ”Dedicated GPU nodes, used only by GPU Pods.” | Taint + toleration and node affinity. |
Practicing Until Placement Is Automatic
Reading YAML is not the same as producing it under a 120-minute clock. In a CKA or CKAD performance task you will taint a node, watch a Pod go Pending, add the right toleration, and confirm it schedules — all while the timer runs. The muscle memory that matters is the debugging loop: kubectl describe pod <name> and reading the Events at the bottom, where the scheduler tells you exactly why a Pod is stuck (“node(s) had untolerated taint,” “didn’t match Pod’s node affinity/selector,” “didn’t satisfy existing pods anti-affinity”). Each message maps to one mechanism above.
The fastest way to build that reflex is repetition against realistic, timed tasks rather than toy examples. Sailor.sh’s KubeAstronaut mock exam bundle gives you performance-based scenarios across CKA, CKAD, CKS, KCNA, and KCSA — including scheduling tasks where you taint nodes, write tolerations, and satisfy affinity rules in a live cluster, then check your work against a reference solution. Pair that hands-on practice with the conceptual grounding in the KCNA scheduling & resource management guide and the general CKA troubleshooting guide, and the placement mechanisms stop being a memorization exercise and start being a tool you reach for automatically.
Frequently Asked Questions
Are taints and node affinity opposites?
No — this is the most common misconception. A taint repels Pods from the node side; node affinity attracts a Pod from the Pod side. They are complementary, not inverse. Dedicated-node setups use both: the taint keeps unwanted Pods off, and the affinity pulls the intended Pods on.
What is the difference between NoSchedule and NoExecute?
NoSchedule prevents new intolerant Pods from being scheduled but leaves already-running Pods alone. NoExecute also evicts intolerant Pods that are already running on the node. If a question mentions eviction or removing existing Pods, the answer is NoExecute.
Does a toleration force a Pod onto a tainted node?
No. A toleration only permits the Pod to schedule there; it does not attract it. The Pod can still land on any other suitable node. To force placement onto specific nodes, combine the toleration with node affinity or a nodeSelector.
When should I use node affinity instead of nodeSelector?
Use nodeSelector for a single simple equality match. Use node affinity when you need set-based operators (In, NotIn, Exists), soft/preferred rules with weights, or multiple weighted preferences. Node affinity is a strict superset of nodeSelector’s capability.
What does IgnoredDuringExecution mean in affinity rules?
It means the rule is enforced only when the Pod is scheduled. If the node’s labels change afterward, a running Pod is not evicted. All current stable affinity rules use this behavior; a RequiredDuringExecution variant that would evict is not yet part of the tested feature set.
What is topologyKey in pod affinity?
It names the node label that defines the “domain” the rule operates over. kubernetes.io/hostname makes the domain a single node (use it to spread across nodes); topology.kubernetes.io/zone makes the domain an availability zone (use it to spread across zones). Pod affinity and anti-affinity are always evaluated relative to that topology, never to raw node names.
Should I use pod anti-affinity or topology spread constraints to spread replicas?
For simple even distribution, prefer topology spread constraints with maxSkew — they are cheaper for the scheduler and easier to reason about. Use required pod anti-affinity only when you need an absolute guarantee that no two matching Pods share a domain, and accept that it may leave Pods Pending.