How to Implement PodAntiAffinity in Kubernetes

How to Implement PodAntiAffinity in Kubernetes

A three-replica Deployment that all landed on the same node isn’t really three replicas — it’s one point of failure wearing a disguise. This is the exact problem PodAntiAffinity solves: it tells the scheduler to keep specific Pods apart, so a single node, zone, or rack failure doesn’t take out an entire service at once.

Why This Matters More Than It Seems

By default, the Kubernetes scheduler already tries to spread replicas reasonably across nodes as a scoring preference — but that’s a soft heuristic, not a guarantee, and it can be overridden by resource pressure or bin-packing behavior. For anything where availability actually matters — a payment API, a primary database, an authentication service — leaving replica placement to chance isn’t good enough. PodAntiAffinity makes the “don’t co-locate” rule explicit and enforceable.

Basic Syntax

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: payment-api
  template:
    metadata:
      labels:
        app: payment-api
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values:
                      - payment-api
              topologyKey: kubernetes.io/hostname
      containers:
        - name: payment-api
          image: registry.example.com/payment-api:1.0.0
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 250m
              memory: 256Mi
            limits:
              cpu: 500m
              memory: 512Mi

This says: no two Pods with app: payment-api may share a node. With required, if the cluster has fewer nodes than replicas, some Pods will stay Pending — which is the correct, safe failure mode for a hard availability requirement, not a bug.

Zone-Level Anti-Affinity

For clusters spanning multiple availability zones, spreading across zones is usually more valuable than spreading across individual nodes (a whole zone going down is a real, recurring failure mode in every major cloud):

      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values:
                        - payment-api
                topologyKey: topology.kubernetes.io/zone

Using preferred here rather than required matters: with 3 replicas across exactly 3 zones, a hard requirement works fine, but the moment you scale to 4 replicas across 3 zones, a required rule leaves the fourth Pod permanently Pending. Preferred lets the scheduler do its best without blocking rollout.

Verifying Spread

kubectl get pods -o wide -l app=payment-api

Output:

NAME                            NODE      NODE-ZONE
payment-api-6f9d7c8b7-abcde     node-1    us-east-1a
payment-api-6f9d7c8b7-fghij     node-4    us-east-1b
payment-api-6f9d7c8b7-klmno     node-7    us-east-1c

If a Pod is stuck Pending due to an anti-affinity conflict:

kubectl describe pod payment-api-6f9d7c8b7-pqrst
Events:
  Warning  FailedScheduling  0/6 nodes are available: 3 node(s) didn't
  match pod anti-affinity rules, 3 node(s) had taints the pod didn't
  tolerate.

Combining with Topology Spread Constraints

topologySpreadConstraints (a separate, more flexible mechanism introduced after PodAntiAffinity) is often the better modern tool for even distribution, especially at scale:

      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: payment-api

maxSkew: 1 guarantees no zone has more than one extra Pod compared to the least-loaded zone — a more precise and scalable guarantee than PodAntiAffinity’s binary “same topology or not.” Many teams now use topology spread constraints for even distribution and reserve PodAntiAffinity for the specific “never co-locate with this other workload” case (e.g., don’t put a Pod on the same node as a batch job known to spike memory usage).

Anti-Affinity Between Different Workloads

PodAntiAffinity isn’t limited to a Pod’s own replicas — it can reference any label, including a different application entirely:

      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: workload-type
                    operator: In
                    values:
                      - batch-heavy
              topologyKey: kubernetes.io/hostname

This keeps latency-sensitive Pods off any node currently running something labeled workload-type: batch-heavy — useful for isolating noisy neighbors without needing dedicated node pools.

RBAC and Deployment Permissions

As with PodAffinity, no special RBAC is needed beyond standard Deployment permissions, since anti-affinity is just part of the Pod spec:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: deployment-editor
  namespace: production
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]

PodDisruptionBudget: The Necessary Companion

Anti-affinity controls placement; PodDisruptionBudget controls voluntary disruption (node drains, cluster upgrades). Both are needed together for real HA:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: payment-api-pdb
  namespace: production
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: payment-api

Without this, a node drain during a cluster upgrade could evict all three well-spread Pods in quick succession anyway, defeating the point of spreading them in the first place.

Troubleshooting Unschedulable Pods

kubectl get pods -n production --field-selector status.phase=Pending
kubectl describe pod <pod-name> -n production
kubectl get nodes -o wide

If Pods are stuck Pending because of required anti-affinity and there genuinely aren’t enough nodes, the fix is either adding nodes, relaxing to preferred, or reducing replica count to match available topology domains.

Anti-Affinity Inside a StatefulSet

Databases and other stateful workloads deployed via StatefulSet benefit even more directly from anti-affinity, since losing two replicas of a quorum-based system (like etcd, Cassandra, or Elasticsearch) to a single node failure can mean losing quorum entirely:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: cassandra
  namespace: production
spec:
  serviceName: cassandra
  replicas: 3
  selector:
    matchLabels:
      app: cassandra
  template:
    metadata:
      labels:
        app: cassandra
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values:
                      - cassandra
              topologyKey: kubernetes.io/hostname
      containers:
        - name: cassandra
          image: cassandra:5.0
          ports:
            - containerPort: 9042
          volumeMounts:
            - name: data
              mountPath: /var/lib/cassandra
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 100Gi

With required anti-affinity here, the scheduler physically cannot place two Cassandra replicas on the same node — a hard guarantee that matches the hard requirement quorum-based systems actually have.

Rolling Updates Interact with Anti-Affinity Too

A subtlety worth knowing: during a rolling update, a Deployment briefly runs both old and new replica Pods simultaneously (governed by maxSurge). If anti-affinity is scoped to match both old and new Pods by a shared label (like app: payment-api), the surge Pod competing for a “free” topology domain can itself become unschedulable mid-rollout on a tightly packed cluster:

kubectl rollout status deployment/payment-api -n production
Waiting for deployment "payment-api" rollout to finish: 1 out of 3 new
replicas have been updated...

If this hangs, checking for exactly this cause is worthwhile:

kubectl get pods -n production -l app=payment-api -o wide
kubectl describe pod <surge-pod-name> -n production

The fix is usually either adding one extra node/zone capacity headroom before rolling out, or setting maxSurge: 0 combined with maxUnavailable: 1 so the rollout replaces Pods one at a time instead of creating a temporary extra Pod that anti-affinity then can’t place.

matchLabelKeys: A Newer Refinement

Kubernetes added matchLabelKeys to affinity terms specifically to address the rolling-update problem above — it lets an anti-affinity rule automatically scope itself to Pods from the same ReplicaSet revision, rather than every Pod matching the label regardless of revision:

      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values:
                      - payment-api
              topologyKey: kubernetes.io/hostname
              matchLabelKeys:
                - pod-template-hash

This means old and new revisions during a rollout are no longer forced to avoid each other’s nodes — only same-revision replicas are kept apart, which is almost always the actual intent.

Scheduling Cost at Scale

Like PodAffinity, PodAntiAffinity requires the scheduler to inspect labels across potentially many other Pods for every scheduling decision, and the cost compounds specifically because anti-affinity checks tend to touch a larger fraction of the cluster than a typical affinity check does — the scheduler has to confirm the absence of a match across every candidate node, not just find one match somewhere. On clusters running into the thousands of Pods, this is a documented, measurable contributor to scheduling latency, and it’s one of the more concrete reasons the Kubernetes project has continued investing in topology spread constraints as a lighter-weight alternative for the common “spread evenly” case, reserving PodAntiAffinity for cases that genuinely need a hard exclusion rule rather than an even distribution.

A practical middle ground on large clusters: use preferred rather than required wherever the exact guarantee isn’t strictly load-bearing, since preferred terms participate in scoring rather than filtering and are considerably cheaper for the scheduler to evaluate at scale.

Anti-Affinity and Cluster Autoscaler Interaction

Worth calling out explicitly: cluster autoscalers (including the standard Kubernetes Cluster Autoscaler) simulate scheduling decisions before deciding whether adding a new node would actually let a pending Pod schedule. Required anti-affinity rules are respected in this simulation, meaning the autoscaler correctly recognizes that a Pod stuck Pending due to an anti-affinity conflict needs a new, currently-empty node, not just any node with spare capacity — and will provision one accordingly, assuming the node group’s constraints allow it. This is one of the more elegant properties of the design: a hard anti-affinity requirement doesn’t just express intent, it actively drives correct autoscaling behavior without any additional configuration.

Common Mistakes

  • Using required anti-affinity with more replicas than available nodes/zones, causing a rollout to hang indefinitely.
  • Anti-affinity rules based on kubernetes.io/hostname when the intent was actually zone-level resilience — a subtle but consequential mismatch.
  • Forgetting to pair anti-affinity with a PodDisruptionBudget, leaving spread Pods vulnerable to simultaneous voluntary eviction during maintenance.
  • Not testing rollout behavior during a scale-up — a working 3-replica anti-affinity setup can suddenly break when scaled to 5 if the topology doesn’t have room.

Summary

PodAntiAffinity is what turns “we have 3 replicas” into “we have 3 replicas that can’t all die from the same failure.” Required rules give hard guarantees at the cost of scheduling flexibility; preferred rules degrade gracefully. Combined with topology spread constraints for even distribution and a PodDisruptionBudget for safe maintenance, it’s one of the more direct levers for real, measurable availability improvement in a cluster.

References

  • Kubernetes affinity and anti-affinity docs: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity
  • Topology spread constraints: https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/
  • Pod Disruption Budgets: https://kubernetes.io/docs/tasks/run-application/configure-pdb/
Total
1
Shares

Leave a Reply

Previous Post
How to Set Up a Highly Available Kubernetes Cluster

How to Set Up a Highly Available Kubernetes Cluster

Next Post
How to Set Up Storage Classes in Kubernetes

How to Set Up Storage Classes in Kubernetes

Related Posts