Node Affinity & Anti-Affinity
Node Affinity, Pod Affinity & Anti-Affinity
TL;DR
- Taints/tolerations repel; affinity actively attracts pods toward or away from nodes and other pods.
- Node affinity matches node labels (replaces
nodeSelectorwith richer expressions). Pod affinity/anti-affinity matches labels on other running pods, scoped bytopologyKey. requiredDuringSchedulingIgnoredDuringExecutionis a hard rule enforced only at scheduling time — it is never re-evaluated against a pod that’s already running.- Required pod anti-affinity with insufficient nodes leaves excess replicas
Pendingforever; there is no auto-provisioning fallback. - Pod affinity/anti-affinity is O(n) per scheduling decision (must enumerate other pods across the topology domain), unlike node affinity’s O(1) node-label check — this is why
topologySpreadConstraintsexists as a cheaper alternative for the common “spread evenly” case.
Core Concepts
Node Affinity: Pod-to-Node Attraction
Node affinity replaces the older, simpler nodeSelector with a richer expression syntax and two enforcement strengths:
requiredDuringSchedulingIgnoredDuringExecution— hard requirement; pod won’t schedule unless satisfied. Existing running pods are not evicted if node labels later change (the “IgnoredDuringExecution” part).preferredDuringSchedulingIgnoredDuringExecution— soft preference with aweight(1-100); scheduler scores nodes and prefers higher-weighted matches but will still place the pod elsewhere if needed.
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: 80
preference:
matchExpressions:
- key: instance-type
operator: In
values: ["compute-optimized"]
containers:
- name: app
image: myorg/app:1.0
Pod Affinity & Anti-Affinity: Pod-to-Pod Attraction/Repulsion
Instead of matching node labels, pod affinity/anti-affinity matches labels of other pods already running on candidate nodes, scoped by topologyKey (usually kubernetes.io/hostname for per-node, or topology.kubernetes.io/zone for per-zone).
Pod Affinity (co-locate — e.g. put a cache next to the app that uses it):
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values: ["cache-redis"]
topologyKey: "kubernetes.io/hostname"
Pod Anti-Affinity (spread — the classic HA pattern: never put two replicas of the same app on one node):
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values: ["payments-api"]
topologyKey: "kubernetes.io/hostname"
Topology Spread Constraints
For simple even-spreading across zones/nodes without the pairwise-comparison overhead of anti-affinity, topologySpreadConstraints is the modern, more scalable alternative:
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: payments-api
maxSkew: 1 means the difference in pod count between the most- and least-loaded zone can never exceed 1.
Node Affinity vs Pod Affinity/Anti-Affinity vs Topology Spread Constraints
| Aspect | Node Affinity | Pod Affinity/Anti-Affinity | Topology Spread Constraints |
|---|---|---|---|
| Matches against | Node labels | Labels of other running pods | Pod counts per topology domain |
| Typical use | Pin to instance type, zone, disk type | Co-locate (affinity) or spread (anti-affinity) relative to other pods | Even distribution across zones/nodes |
| Scheduler cost | O(1) per node — inspects only that node’s labels | O(n) — must enumerate other pods across the topology domain | Cheaper than anti-affinity; tracks aggregate counts, not pairwise matches |
Failure mode when unsatisfiable (required) |
Pod stays Pending |
Pod stays Pending |
Configurable via whenUnsatisfiable: DoNotSchedule (hard) or ScheduleAnyway (soft) |
| Scale behavior | Scales cleanly to large clusters | Degrades scheduling throughput at high pod counts | Designed to scale better than anti-affinity for the “spread evenly” case |
Command / Configuration Reference
Label a node to enable node affinity targeting:
kubectl label nodes worker-1 disktype=ssd
Check which zone/topology labels a node already carries (usually set by the cloud provider):
kubectl get node worker-1 -o jsonpath='{.metadata.labels}' | jq
Verify actual pod distribution across nodes after applying anti-affinity:
kubectl get pods -o wide -l app=payments-api
5. Architectural Interview Q&A
Q: Your 3-replica Deployment uses requiredDuringSchedulingIgnoredDuringExecution pod anti-affinity on kubernetes.io/hostname, but you only have 2 worker nodes. What happens?
A: The third replica stays Pending forever — a hard anti-affinity requirement cannot be violated, and there’s no third node to satisfy “one pod per node.” This is a common production incident during cluster scale-down or node maintenance: either add nodes, relax to preferredDuringSchedulingIgnoredDuringExecution, or use topologySpreadConstraints with whenUnsatisfiable: ScheduleAnyway.
Q: Why is pod affinity/anti-affinity described as more computationally expensive than node affinity at scale?
A: Node affinity only needs the scheduler to inspect the candidate node’s own labels — O(1) per node. Pod affinity/anti-affinity requires the scheduler to enumerate all other pods across the topology domain to check label matches for every scheduling decision, which is O(n) per pod and degrades scheduling throughput on clusters with tens of thousands of pods. This is precisely why topologySpreadConstraints was introduced as a more scalable alternative for the common “spread evenly” case.
6. Common Pitfalls
[!WARNING] IgnoredDuringExecution Means No Auto-Rebalancing Both node and pod affinity only affect scheduling at pod-creation time. If you relabel a node afterward so it no longer matches a running pod’s
requiredDuringSchedulingrule, Kubernetes does not evict or reschedule that pod — the rule is enforced only during scheduling, then ignored for the pod’s remaining lifetime. If you need pods to actively migrate off nodes that no longer match, you need an external mechanism (descheduler) or must delete the pods manually to force rescheduling.