TL;DR
- Node affinity picks nodes by their labels. Pod affinity picks them by which pods are already running there.
- Affinity pulls a pod toward pods that match a label selector. Anti-affinity pushes it away from them.
-
topologyKeydecides what "together" means: the same node, the same zone, or something wider. - Both come as
requiredorpreferred, and both areIgnoredDuringExecution, same as node affinity. - Strict anti-affinity can leave pods
Pendingonce you run out of room, and these rules cost the scheduler more work than node affinity does.
Skip around if you already know the basics:
- The problem this actually solves
- Affinity, anti-affinity, and topologyKey
- Where this actually shows up
- What the scheduler's doing under the hood
- Pod affinity vs node affinity
- What goes wrong, and why
- Try it yourself
WHY: the problem
In Part 2, a pod picked nodes by their labels. In Part 3, a node pushed pods away with a taint.
Sometimes neither of those fits, because the right placement depends on what else is running. A web app may respond faster when it sits next to its cache. Three replicas of a service don't give you much if all three land on the same node and that node goes down.
Pod affinity and anti-affinity are how you describe those relationships to the scheduler.
WHAT: the two rules and topologyKey
Think of a wedding. The guests are pods, the tables are nodes, and the scheduler is the person with the seating chart. Guests get seated one at a time as they arrive.
- Pod affinity is "seat me with my family." You're asking to land near specific pods that are already there.
- Pod anti-affinity is "please don't seat me with my ex." You're asking to stay away from specific pods.
-
topologyKeyis how wide "together" is. Same table, or just the same room? Same table iskubernetes.io/hostname, which means the same node. Same room is usuallytopology.kubernetes.io/zone. - Required vs preferred is the difference between "this has to happen" and "I'd like that, but I'll cope."
Both rules pick the pods they care about with a labelSelector. Here's the cache-and-web case, where the web pod has to share a node with the cache:
# cache-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: cache
labels:
app: cache
spec:
containers:
- name: redis
image: redis
---
# web-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: web
labels:
app: web
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- cache
topologyKey: kubernetes.io/hostname
containers:
- name: nginx
image: nginx
Anti-affinity has its own field, podAntiAffinity, and works the same way. This Deployment refuses to put two of its own replicas on the same node:
# spread-web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: spread-web
spec:
replicas: 2
selector:
matchLabels:
app: spread-web
template:
metadata:
labels:
app: spread-web
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- spread-web
topologyKey: kubernetes.io/hostname
containers:
- name: nginx
image: nginx
Both of those use required. The preferred version wraps the rule in a podAffinityTerm and adds a weight from 1 to 100, just like preferred node affinity from Part 2. This one tries to keep web pods in different zones, but doesn't fail if it can't:
apiVersion: v1
kind: Pod
metadata:
name: web-zone-spread
labels:
app: web
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web
topologyKey: topology.kubernetes.io/zone
containers:
- name: nginx
image: nginx
A few details worth knowing:
- The
labelSelectorsupportsIn,NotIn,Exists, andDoesNotExist. There's noGtorLthere like there is for node affinity. - By default, a rule only looks at pods in the same namespace as the pod being scheduled. You can widen that with
namespacesornamespaceSelector. -
topologyKeyis required, and it has to be a label your nodes actually have.
📖 Docs: Inter-pod affinity and anti-affinity
WHEN: where this actually shows up
A few places this comes up:
- Putting an app next to its cache or database to cut latency, or to avoid cross-zone network costs
- Spreading replicas across nodes, so one node failing doesn't take the whole service with it
- Spreading replicas across zones, for the same reason one level up
- Keeping two heavy workloads off the same node when they'd get in each other's way
If none of that sounds like your situation, the scheduler's default placement is usually fine.
HOW: what's actually happening
Required rules get checked during filtering, and preferred ones during scoring, same as everything else so far.
What's different is what the scheduler has to look at. Node affinity only needs a node's labels. Pod affinity needs to know which pods are already running on each node, and which zone or other domain that node belongs to, and then it checks those pods against your selector. That's extra work on every scheduling decision, and it's why the Kubernetes docs recommend against using these rules in clusters larger than several hundred nodes.
Because the scheduler only counts pods that are already placed, order matters. Back at the wedding: if your family hasn't arrived yet, there's nobody to sit with, so you wait. A pod with required affinity to a cache that isn't running yet stays Pending until a matching pod shows up.
Anti-affinity also works in both directions. If a running pod has a required anti-affinity rule that matches the new pod, the new pod can't land next to it either, even if the new pod has no rules of its own. Your ex asked not to share a table with you, so that table is off the list, whether or not you'd have minded.
DIFFERENCE: pod affinity vs node affinity
Node affinity is about what a node is. It has an SSD, or it's in a certain zone. Pod affinity is about who's already there. At the wedding, that's the difference between "seat me at a table by the window" and "seat me with my family."
That connects the last few parts. Node affinity pulls a pod toward a kind of node. Taints keep pods off a node unless they tolerate it. Pod affinity and anti-affinity react to the neighbors.
One more link back to Part 2: node anti-affinity isn't its own field, you get it from NotIn or DoesNotExist. Pod anti-affinity does have its own field, podAntiAffinity.
If all you want is an even spread of replicas, topology spread constraints are built for exactly that and are worth a look. They're a separate feature, so they're outside this article.
FAILURE: what goes wrong
The classic one is: required anti-affinity on kubernetes.io/hostname with more replicas than you have nodes. Every node ends up with a replica, the next one has nowhere to go, and it sits in Pending. The scheduling event says something like "didn't match pod anti-affinity rules."
Rolling updates can hit the same wall. During an update, the new pod carries the same label as the old ones, so strict anti-affinity treats the old pods as neighbors to avoid. If every node already has one, the new pod can't start and the rollout stalls. Newer Kubernetes versions have matchLabelKeys for this. Add pod-template-hash to it and the rule only compares pods from the same rollout, so old pods stop blocking new ones. Switching the rule to preferred also works if you can live without the guarantee.
Then there's the topology key. Every node needs the label you name in topologyKey. kubernetes.io/hostname is set automatically, but zone labels usually come from a cloud provider, so on a local or kubeadm cluster they might not exist unless you add them. Missing labels can lead to unexpected placement.
Namespaces catch people too. A selector only matches pods in the pod's own namespace unless you say otherwise, so a rule aimed at pods in another namespace matches nothing. With required affinity that means Pending. With anti-affinity it means the spreading quietly doesn't happen.
And like node affinity, these rules are IgnoredDuringExecution. If labels change after a pod is placed, nothing moves.
PRACTICE: deploy, scale, check
You'll need at least two worker nodes for this.
Start with affinity. Save the two pods from the WHAT section as cache-pod.yaml and web-pod.yaml, then apply them:
kubectl apply -f cache-pod.yaml
kubectl apply -f web-pod.yaml
kubectl get pods -o wide
Both should land on the same node. Now remove them and start only the web pod:
kubectl delete pod cache web
kubectl apply -f web-pod.yaml
kubectl get pods
It should sit in Pending. The Events section shows why:
kubectl describe pod web
Look for a FailedScheduling event that mentions pod affinity. Apply cache-pod.yaml again and the web pod should get scheduled next to it.
Now anti-affinity. Save the Deployment as spread-web.yaml, apply it, and check that the replicas landed on different nodes:
kubectl apply -f spread-web.yaml
kubectl get pods -o wide
Then scale it past your worker node count. If you have two workers, try five:
kubectl scale deployment spread-web --replicas=5
kubectl get pods
The extra replicas should stay Pending, and kubectl describe pod on one of them will show the anti-affinity reason. Clean up when you're done:
kubectl delete -f cache-pod.yaml -f web-pod.yaml -f spread-web.yaml --ignore-not-found
Next up, Part 5: DaemonSets, where a pod gets placed on every node, and the taints from Part 3 show up again.
If anything here didn't land, or you'd explain it differently, drop a comment. Doubts and pushback are both welcome.
Top comments (0)