πŸ“šBook Signing at KubeCon EU 2026Meet us at Booking.com HQ (Mon 18:30-21:00) & vCluster booth #521 (Tue 24 Mar, 12:30-1:30pm) β€” free book giveaway!RSVP Booking.com Event
Deployments intermediate ⏱ 12 minutes K8s 1.28+

K8s Node Affinity and Pod Scheduling

Configure Kubernetes node affinity, pod affinity, and anti-affinity rules. nodeSelector, requiredDuringScheduling, preferredDuringScheduling, and topology.

By Luca Berton β€’ β€’ πŸ“– 5 min read

πŸ’‘ Quick Answer: nodeSelector: {disk: ssd} schedules pods to nodes with matching labels (hard constraint). For flexible rules, use nodeAffinity with requiredDuringSchedulingIgnoredDuringExecution (hard) or preferredDuringSchedulingIgnoredDuringExecution (soft). Pod affinity/anti-affinity co-locates or spreads pods relative to other pods. topologySpreadConstraints distributes pods evenly across zones/nodes.

The Problem

Default scheduling places pods on any available node:

  • GPU workloads land on CPU-only nodes
  • Pods for the same service all land on one node (no HA)
  • Latency-sensitive pods run on distant nodes
  • Compliance requires workloads in specific zones

The Solution

nodeSelector (Simple)

apiVersion: v1
kind: Pod
metadata:
  name: gpu-pod
spec:
  nodeSelector:
    gpu-type: a100        # Must match node label exactly
    disk: ssd
  containers:
  - name: training
    image: pytorch:latest
# Label a node
kubectl label node worker-1 gpu-type=a100 disk=ssd

# Check node labels
kubectl get nodes --show-labels

Node Affinity (Flexible)

apiVersion: v1
kind: Pod
metadata:
  name: web-app
spec:
  affinity:
    nodeAffinity:
      # Hard requirement β€” must match
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: zone
            operator: In
            values: ["us-east-1a", "us-east-1b"]
          - key: instance-type
            operator: NotIn
            values: ["t3.micro"]
      
      # Soft preference β€” prefer but don't require
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 80
        preference:
          matchExpressions:
          - key: disk
            operator: In
            values: ["ssd"]
      - weight: 20
        preference:
          matchExpressions:
          - key: spot
            operator: DoesNotExist
  containers:
  - name: web
    image: nginx:1.27

Operators

OperatorMeaning
InLabel value in list
NotInLabel value not in list
ExistsLabel key exists (any value)
DoesNotExistLabel key doesn’t exist
GtGreater than (numeric)
LtLess than (numeric)

Pod Affinity / Anti-Affinity

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-frontend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web-frontend
  template:
    metadata:
      labels:
        app: web-frontend
    spec:
      affinity:
        # Co-locate with cache pods (same node)
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels:
                app: redis-cache
            topologyKey: kubernetes.io/hostname
        
        # Spread frontend replicas across nodes
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchLabels:
                  app: web-frontend
              topologyKey: kubernetes.io/hostname
      containers:
      - name: web
        image: frontend:v2

Topology Spread Constraints

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
spec:
  replicas: 6
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      topologySpreadConstraints:
      # Spread evenly across zones
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: web
      # Also spread across nodes within zones
      - maxSkew: 1
        topologyKey: kubernetes.io/hostname
        whenUnsatisfiable: ScheduleAnyway
        labelSelector:
          matchLabels:
            app: web
      containers:
      - name: web
        image: nginx:1.27
# Result with 6 replicas across 3 zones, 6 nodes:
# Zone A: node-1 [web] node-2 [web]
# Zone B: node-3 [web] node-4 [web]
# Zone C: node-5 [web] node-6 [web]

Common Issues

Pod stuck in Pending β€” β€œdidn’t match node affinity”

No nodes match the required affinity rules. Check: kubectl get nodes -l <label> and kubectl describe pod <pod>.

Anti-affinity too strict β€” can’t schedule all replicas

With requiredDuringScheduling anti-affinity on hostname, you need at least as many nodes as replicas. Use preferredDuringScheduling instead.

topologySpreadConstraints with DoNotSchedule blocks scheduling

maxSkew: 1 is strict. Increase maxSkew or use ScheduleAnyway for soft spreading.

Best Practices

  • nodeSelector for simple cases β€” GPU nodes, SSD nodes
  • nodeAffinity for complex rules β€” multiple values, preferences
  • podAntiAffinity for HA β€” spread replicas across nodes/zones
  • topologySpreadConstraints over pod anti-affinity β€” more granular control
  • Use preferred over required when possible β€” keeps scheduling flexible

Key Takeaways

  • nodeSelector: simple label matching (hard constraint)
  • nodeAffinity: flexible with In/NotIn/Exists operators and soft preferences
  • podAffinity: co-locate pods on same node/zone as other pods
  • podAntiAffinity: spread pods away from each other (HA)
  • topologySpreadConstraints: distribute evenly across zones/nodes with maxSkew
#scheduling #node-affinity #pod-affinity #topology #cka
Luca Berton
Written by Luca Berton

Principal Solutions Architect specializing in Kubernetes, AI/GPU infrastructure, and cloud-native platforms. Author of Kubernetes Recipes and creator of CopyPasteLearn courses.

Kubernetes Recipes book cover

Want More Kubernetes Recipes?

This recipe is from Kubernetes Recipes, our 750-page practical guide with hundreds of production-ready patterns.

Luca Berton Ansible Pilot Ansible by Example Open Empower K8s Recipes Terraform Pilot CopyPasteLearn ProteinLens