Kubernetes node affinity is surprisingly less about where pods should go and more about where they absolutely must not go.

Let’s see it in action. Imagine you have a couple of nodes, node-1 and node-2, and you want to ensure a specific application pod, my-app, only ever lands on nodes that have a disktype=ssd label.

Here’s how you’d express that in your pod’s YAML:

apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
  - name: app-container
    image: nginx:latest
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: disktype
            operator: In
            values:
            - ssd

When you apply this, Kubernetes’ scheduler looks at my-app. It sees the requiredDuringSchedulingIgnoredDuringExecution rule. This means: "During scheduling, I must satisfy this. If the node’s labels change later (ignored during execution), that’s fine, but the initial placement is critical." It then checks all available nodes. Only node-1 (if it has disktype=ssd) or node-2 (if it has disktype=ssd) will be considered. If neither node has that label, my-app will remain pending indefinitely.

This solves the problem of needing specific hardware for certain workloads. Maybe you have expensive GPUs, or high-performance storage, or nodes in a particular network zone that your application must run on for performance or compliance. Node affinity lets you enforce these constraints.

Internally, when a pod is created, the Kubernetes scheduler evaluates all potential nodes against the pod’s affinity and anti-affinity rules. For node affinity, it’s looking for nodes that match the specified label selectors. requiredDuringSchedulingIgnoredDuringExecution is the strongest form; the pod will not be scheduled if no node satisfies the rule. preferredDuringSchedulingIgnoredDuringExecution is softer; the scheduler will try to satisfy it but will schedule the pod even if it can’t.

The operator field is key here. In means the node’s label value must be one of the values in the values list. Other operators include NotIn (the label must not be any of the listed values), Exists (the label must exist, regardless of its value), and DoesNotExist (the label must not exist).

Consider a more complex scenario. You have a cluster with nodes tagged for environment=production and environment=staging. You want your frontend-api pod to only run on production nodes but never on nodes running the auth-service pod. This requires both node affinity and pod anti-affinity.

apiVersion: v1
kind: Pod
metadata:
  name: frontend-api
spec:
  containers:
  - name: api-container
    image: my-api-image:v1.2
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: environment
            operator: In
            values:
            - production
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - auth-service
        topologyKey: "kubernetes.io/hostname"

Here, frontend-api is restricted to nodes with environment=production. Simultaneously, the podAntiAffinity rule, with topologyKey: "kubernetes.io/hostname", ensures that frontend-api won’t be scheduled on any node that already has a pod with the label app=auth-service running on it. This prevents noisy neighbor issues or dependency conflicts.

The topologyKey in pod anti-affinity is crucial for defining the "scope" of the rule. kubernetes.io/hostname means "don’t schedule on the same node." Other common keys include topology.kubernetes.io/zone (don’t schedule pods across different availability zones) or topology.kubernetes.io/region.

Most people miss that requiredDuringSchedulingIgnoredDuringExecution is the default and most common form for requiredDuringScheduling. The "IgnoredDuringExecution" part is key: if a node’s labels change after the pod is scheduled, and that change would violate the affinity rule, the pod keeps running. Kubernetes doesn’t evict it. This is a deliberate design choice to prioritize workload stability over strict, real-time label adherence post-scheduling. It means your affinity rules are primarily for initial placement, not for continuous enforcement of dynamic node states.

The next step is understanding how to combine node affinity with other scheduling constraints like taints and tolerations.

Want structured learning?

Take the full Kubernetes course →