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](/docs/hetzner/apalla/workloads/placement/steer-workload-placement) 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: ```yaml # 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"] ``` ```yaml # 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](/docs/hetzner/apalla/servers-and-nodes/scheduling/node-affinity-and-selectors). ## 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: ```yaml # Co-locate with a cache to cut latency (soft) spec: affinity: podAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: {app: cache} topologyKey: kubernetes.io/hostname ``` ```yaml # 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](/docs/hetzner/apalla/workloads/placement/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.