Node affinity is the expressive successor to nodeSelector. While nodeSelector uses simple key=value label matching, node affinity supports operators (In, NotIn, Exists, DoesNotExist, Gt, Lt), soft preferences with weights, and the distinction between hard requirements and soft preferences.
1. nodeSelector vs Node Affinity
# nodeSelector — simple, limited:
spec:
nodeSelector:
disktype: ssd
zone: us-east-1a
# Pod MUST go to a node with BOTH labels. No "or", no "prefer", no operators.
# Node affinity — powerful, expressive:
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution: ... # Hard
preferredDuringSchedulingIgnoredDuringExecution: ... # Soft
| Feature | nodeSelector | Node Affinity |
|---|---|---|
| Operators | Only exact match (=) | In, NotIn, Exists, DoesNotExist, Gt, Lt |
| Hard/Soft | Hard only (must match) | Both hard (required) and soft (preferred) |
| Weight | No | Yes — weighted scoring for soft rules |
| OR logic | No (all must match) | Yes (multiple nodeSelectorTerms = OR) |
| Use case | Simple constraints | Complex placement logic |
2. Required Node Affinity (Hard)
requiredDuringSchedulingIgnoredDuringExecution — the Pod must schedule on a matching node, or stays Pending.
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms: # OR between terms
- matchExpressions: # AND between expressions in same term
- key: topology.kubernetes.io/zone
operator: In
values:
- us-east-1a
- us-east-1b
- key: node.kubernetes.io/instance-type
operator: In
values:
- m5.xlarge
- m5.2xlarge
Logic Rules
- Multiple
nodeSelectorTerms→ OR (match any one term) - Multiple
matchExpressionswithin one term → AND (all must match)
# This means: Node must be in (us-east-1a OR us-east-1b) AND (m5.xlarge OR m5.2xlarge) # If you want "zone a with m5" OR "zone c with c5", use two separate nodeSelectorTerms
Available Operators
| Operator | Meaning | Example |
|---|---|---|
In | Label value is in the list | zone In [us-east-1a, us-east-1b] |
NotIn | Label value is NOT in the list | instance-type NotIn [t3.micro] (avoid small) |
Exists | Label key exists (any value) | gpu Exists (has a GPU label) |
DoesNotExist | Label key doesn't exist | dedicated DoesNotExist (not a dedicated node) |
Gt | Label value > given integer | gpu-count Gt 2 |
Lt | Label value < given integer | age Lt 30 |
NotIn is how you express anti-affinity to nodes. There's no separate "node anti-affinity" resource — you use NotIn or DoesNotExist operators within node affinity to repel Pods from certain nodes. Example: operator: NotIn, values: [spot] keeps the Pod off spot instances.
3. Preferred Node Affinity (Soft)
preferredDuringSchedulingIgnoredDuringExecution — the scheduler tries to match, but schedules elsewhere if no matching node is available. Uses weights to express preference strength.
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80 # 1-100: higher = stronger preference
preference:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values: ["us-east-1a"] # Strongly prefer zone a
- weight: 20
preference:
matchExpressions:
- key: node.kubernetes.io/instance-type
operator: In
values: ["m5.xlarge"] # Weakly prefer m5.xlarge
How Weights Work
# The scheduler scores each node: # For each preference that matches → add its weight to the node's score # # Node A: zone=us-east-1a, type=m5.xlarge → score += 80 + 20 = 100 # Node B: zone=us-east-1a, type=c5.xlarge → score += 80 + 0 = 80 # Node C: zone=us-east-1b, type=m5.xlarge → score += 0 + 20 = 20 # Node D: zone=us-east-1b, type=c5.xlarge → score += 0 + 0 = 0 # # Scheduler picks Node A (highest score) # But if Node A is full, it can still use B, C, or D (soft, not hard)
Combining Required + Preferred
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution: # MUST
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values: [us-east-1a, us-east-1b, us-east-1c] # Must be in these zones
preferredDuringSchedulingIgnoredDuringExecution: # PREFER
- weight: 70
preference:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values: [us-east-1a] # Prefer zone a among the allowed zones
# Result: Must be in zone a/b/c. Strongly prefers a. Falls back to b/c if a is full.
required to enforce hard constraints (correct region, correct instance family) and preferred to optimize within those bounds (prefer the cheaper zone, prefer nodes with image cache). Pods always schedule (within the hard constraints) but land in the best spot when possible.
4. "IgnoredDuringExecution" — What It Means
The full name requiredDuringSchedulingIgnoredDuringExecution means:
- During scheduling: The rule is enforced (required or preferred)
- During execution: If node labels change after scheduling, the Pod stays — it's NOT evicted
# Example: Pod requires zone=us-east-1a # Pod schedules on node in us-east-1a ✓ # Admin removes the zone label from the node # Pod STAYS — the rule is only checked at scheduling time
A future K8s feature (requiredDuringSchedulingRequiredDuringExecution) would evict Pods when labels change — but it doesn't exist yet.
5. Common Node Labels for Affinity
| Label | Example Value | Use Case |
|---|---|---|
topology.kubernetes.io/zone | us-east-1a | Zone placement, HA distribution |
topology.kubernetes.io/region | us-east-1 | Regional constraints |
node.kubernetes.io/instance-type | m5.2xlarge | Instance family selection |
kubernetes.io/arch | amd64, arm64 | Architecture-specific images |
kubernetes.io/os | linux, windows | OS-specific workloads |
node.kubernetes.io/disk-type | ssd, hdd | Storage-sensitive workloads |
Custom: team | platform | Dedicated team nodes |
Custom: gpu | nvidia-a100 | ML workloads |
# See all labels on a node: kubectl get node worker-1 --show-labels # Add a custom label: kubectl label node worker-3 gpu=nvidia-a100
topology.kubernetes.io/zone for zone-aware scheduling and custom labels for nodeSelector/affinity. Know how to label nodes (kubectl label node) and write both required and preferred affinity rules. The YAML is verbose — practice until you can write it from memory.
6. nodeSelector vs Affinity — When to Use Which
| Scenario | Use | Why |
|---|---|---|
| Simple: "must be on SSD nodes" | nodeSelector: {disktype: ssd} | Cleaner, shorter YAML |
| "Must be in zone a OR b" (not c) | Required affinity with In | nodeSelector can't do OR |
| "Prefer zone a, fall back to others" | Preferred affinity with weight | nodeSelector is hard only |
| "Must NOT be on spot instances" | Required affinity with NotIn | nodeSelector can't negate |
| "Prefer large instances, accept small" | Preferred affinity | Soft preference impossible with nodeSelector |
Summary
| Concept | Key Point |
|---|---|
| nodeSelector | Simple label matching (AND logic, exact match only) |
| Required affinity | Must match — Pod stays Pending if no node qualifies |
| Preferred affinity | Best-effort — scheduler scores nodes, picks highest weight match |
| Weights (1-100) | Higher = stronger preference. Scores are summed across preferences. |
| nodeSelectorTerms | Multiple terms = OR logic |
| matchExpressions | Multiple in same term = AND logic |
| Operators | In, NotIn, Exists, DoesNotExist, Gt, Lt |
| IgnoredDuringExecution | Rules only enforced at scheduling time — label changes don't evict |
| NotIn = anti-affinity | No separate "node anti-affinity" — use NotIn/DoesNotExist operators |
📝 Quiz: Node Affinity
Q1: A Pod has requiredDuringSchedulingIgnoredDuringExecution with zone In [us-east-1a]. No node has that zone label. What happens?
Q2: A Pod has preferred affinity: weight 90 for zone=a, weight 10 for type=m5. Node X is in zone a with type c5. Node Y is in zone b with type m5. Which is chosen?
Q3: What's the difference between two matchExpressions in one nodeSelectorTerm vs two separate nodeSelectorTerms?
Separate terms (OR): The node must match ANY one complete term. E.g., (zone=us-east-1a AND type=m5) OR (zone=us-west-2a AND type=c5).
Q4: How do you express "don't schedule on spot instances" using node affinity?
NotIn or DoesNotExist operator:requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node.kubernetes.io/lifecycle
operator: NotIn
values: ["spot"]Or if spot nodes have a label key spot:operator: DoesNotExist, key: spotQ5: A Pod is running on a node labeled zone=us-east-1a. The admin removes this label. The Pod has required affinity for zone In [us-east-1a]. Is the Pod evicted?
requiredDuringSchedulingIgnoredDuringExecution. Once the Pod is running, affinity rules are not re-evaluated. The Pod stays on the node even if labels change. Only NoExecute taints can evict running Pods based on changed conditions.Q6: When should you use nodeSelector instead of node affinity?