Set affinity and anti-affinity
Affinity is the more expressive cousin of the node selector. Node affinity places a pod relative to node labels, with both hard and soft rules; pod affinity places a pod relative to other pods, to pull them together or push them apart. Use it when a plain node selector cannot say what you mean.
Anti-affinity and spread exist for one blunt reason: nodes get drained and replaced. If three replicas of a service all landed on the same host, a single node going away takes all three with it. So you spread them on purpose. One caveat before the recipes: all of these rules read IgnoredDuringExecution, meaning they are evaluated once, at scheduling time. A pod already running is never re-placed when labels change underneath it.
Node affinity: require or prefer#
Two forms, and the whole difference is what happens when nothing matches:
# Hard rule: only bare-metal nodes, or stay Pending
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: instance.hetzner.cloud/is-root-server
operator: In
values: ["true"]
# Soft rule: prefer a zone, run anywhere if it is full
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values: [hel1-dc2]
required... is a filter: no matching node means the pod sits Pending. preferred... is a nudge carrying a weight, so the scheduler tries to honor it but places the pod elsewhere rather than leave it stuck. matchExpressions accepts In, NotIn, Exists, DoesNotExist, Gt, and Lt.
These rules read the labels a node already carries. For which system labels you can select on and the empty-string role-label gotcha, see Node affinity and selectors in practice .
Pod affinity and anti-affinity#
Pod affinity places a pod relative to other pods that carry a label, pulling them together or pushing them apart:
# Co-locate with a cache to cut latency (soft)
spec:
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels: {app: cache}
topologyKey: kubernetes.io/hostname
# One replica per node (hard), for quorum members
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels: {app: my-database}
topologyKey: kubernetes.io/hostname
Warning
Required host anti-affinity allows at most one matching pod per node. Ask for more replicas than you have nodes and the extras stay Pending forever.
Pod affinity rules are not free. The scheduler weighs each candidate pod against every already-placed pod, so heavy use across a large cluster gets expensive fast. Keep the selectors tight, and do not spray these rules on workloads that do not need them.
When required anti-affinity strands replicas#
The Pending behavior above is correct for a quorum-based database that must never put two members on one machine, and wrong for a stateless service you only wanted spread for availability.
For plain availability spread, prefer topology spread constraints : they keep replicas balanced as the node count moves, without stranding pods when replicas briefly outnumber nodes. Reserve required host anti-affinity for the strict one-per-node case where a collision is genuinely unacceptable.
Steer workload placement
A decision table that routes you to the right placement control, plus the default node and zone spread that already runs before you set anything.
Spread pods with topology constraints
Keep replicas on separate nodes and zones so a single node replacement takes at most one of them.