Sign in
← Kubernetes: scheduling
6 min · medium · Lesson 4 of 7

Node Affinity: required and preferred

Hard and soft placement rules, the matchExpressions operators, and where the OR hides in nodeSelectorTerms.

How to tell the scheduler "only on nodes like this" and "better on nodes like this, but others are fine too", and where OR hides in the YAML.

Why this matters

nodeSelector only knows strict equality: a key and a value. When both an SSD node and an NVMe node work, it can't express that. "Zone A is better, but anything will do" can't be expressed either: nodeSelector either admits the Pod to a node or it doesn't. nodeAffinity exists for rules like these.

What nodeAffinity is

nodeAffinity solves the same problems as nodeSelector, but does more: operators beyond equality, soft rules (preferences) and OR between groups of conditions. The field lives in spec.affinity.nodeAffinity on a Pod, and in a Deployment it's in the Pod template: spec.template.spec.affinity.nodeAffinity.

nodeSelector can be written through affinity: it's a required rule with one In expression and one value. If a Pod has both fields set, the node has to match both nodeSelector and affinity.

Required and preferred

node-1 · ssd · zone=a app node-2 · nvme · weight 0 node-3 · hdd · excluded
Required kept ssd and nvme, preferred by zone=a brought the Pod to node-1.

Two modes:

IgnoredDuringExecution means: if the node's labels change after the Pod starts, the Pod isn't touched. The rules are checked only at placement time.

The variant requiredDuringSchedulingRequiredDuringExecution, which would evict the Pod when labels change, is documented as planned and isn't implemented in Kubernetes.

Minimal YAML

spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: disk
                operator: In
                values: [ssd, nvme]
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 50
          preference:
            matchExpressions:
              - key: zone
                operator: In
                values: [a]

Required and preferred can be set together. Required filters the nodes (only SSD or NVMe), preferred raises the score of nodes with zone=a.

Note the shape. In required, the rules sit inside nodeSelectorTerms, which is a list. In preferred, there is a list of rules, each with its own weight and one preference.

nodeSelectorTerms: where it's AND, and where it's OR

nodeSelectorTerms is an array. Between different terms it's OR: a node only needs to match any one of them. Inside one term, expressions from matchExpressions are combined with AND: a node needs to match all of them at once.

So a term is an AND block, and an array of terms is an OR block. "Either a node with GPU, or a node with the label fallback=cpu" is written as two terms:

nodeSelectorTerms:
  - matchExpressions:
      - key: gpu
        operator: Exists
  - matchExpressions:
      - key: fallback
        operator: In
        values: [cpu]

If you need OR on values of one key, a single expression with a list is enough: operator: In and values: [ssd, nvme].

It's easy to mix up these two levels, and the mistake is silent. If you write both expressions under one dash, - matchExpressions:, they get joined with AND. No node will match, the Pod will sit in Pending, and the API won't say a word: the manifest is valid.

matchExpressions operators

NotIn and DoesNotExist give you "anti-affinity to nodes" without taints. That's handy when you're not a cluster admin, can't set a taint on nodes, but want to stay away from some group of them. NotIn also matches a node without the key at all: there's no label, so it isn't equal to the banned value.

How this looks in the cluster

The effect of preferred rules only shows up where the scheduler actually has a choice. In a single-node cluster, scoring changes nothing: the Pod lands on the only node there is, even if no preference matched. Required works there too, though: if a label doesn't match, the Pod stays Pending.

The reason is visible in kubectl describe pod, the Events section, the same text as for nodeSelector:

Warning  FailedScheduling  ...  default-scheduler  0/1 nodes are available: 1 node(s) didn't match Pod's node affinity/selector. ...

If a Pod is stuck in Pending and you add the missing label to a node, the scheduler places it on its next attempt. But if the Pod is already running, changing labels won't move it. To make the rules apply again, you need a new Pod: for a Deployment that's kubectl rollout restart deployment/<name>.

Common mistakes

kubectl get nodes -L disk -L zone
kubectl get nodes --show-labels
kubectl get pod <name> -o jsonpath='{.spec.affinity}'
kubectl get deployment <name> -o yaml        # the spec.template.spec.affinity section
kubectl describe pod <name>                  # the Events section, if Pending
kubectl rollout restart deployment/<name>    # recreate the Pod under the new rules
kubectl explain pod.spec.affinity.nodeAffinity

What you'll need for the tasks

The tasks are in the labs/ folder: each lab brings up its own kind cluster on your machine, with instructions in its README.md.

Homework

Practice the theory on your own cluster: 2 steps with a check. Set up the cluster with the Kubernetes locally track, the tasks live in the k8s-practice repository.

./run.sh start k8s-node-affinity     # set up the starting state
./run.sh verify k8s-node-affinity    # check the current step

In short

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson k8s-scheduling-04-node-affinity