## Intro

Node affinity in Kubernetes lets you control which nodes your pods land on based on node labels. It is more expressive than the older nodeSelector field and supports both hard requirements and soft preferences. This article explains the architecture behind node affinity, walks through practical examples, and shows how to verify, diagnose, and recover from common scheduling issues.

You will learn how the kube-scheduler evaluates node affinity rules, how to write manifests with required and preferred terms, and how to combine affinity with taints, tolerations, and topology spread for robust scheduling. Every example includes commands and expected output so you can reproduce the scenarios in your own cluster.

## Version and Environment Inventory

Before using node affinity, check your cluster version and the scheduling components involved. Node affinity has been stable since Kubernetes 1.6 and is available in all modern distributions. The examples in this article assume Kubernetes 1.25 or later and a working kubectl context.

### Identify your cluster version

Run:

kubectl version --short 
 Expected output (example):

Client Version: v1.28.2
Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3
Server Version: v1.28.3 
 If your server version is older than 1.6, upgrade before using node affinity. Most current clusters satisfy this requirement.

### Understand the kube-scheduler role

The kube-scheduler is the component that reads pod specifications and finds a suitable node. It runs as a static pod in the kube-system namespace on control plane nodes. Confirm it is running:

kubectl get pods -n kube-system -l component=kube-scheduler 
 Expected output shows a running scheduler pod, for example:

NAME READY STATUS RESTARTS AGE
kube-scheduler-control-plane-1 1/1 Running 0 12d 
 The scheduler uses a scoring algorithm that considers node affinity rules during the filtering and scoring phases. Hard rules (requiredDuringSchedulingIgnoredDuringExecution) act as filters, eliminating nodes that do not match. Soft rules (preferredDuringSchedulingIgnoredDuringExecution) add weight during scoring, giving matching nodes a higher rank but not excluding others.

### Basic node labeling

Node affinity matches pod requirements against node labels. List current labels on your nodes:

kubectl get nodes --show-labels 
 Example output:

NAME STATUS ROLES AGE VERSION LABELS
node-1 Ready control-plane 10d v1.28.3 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=node-1,node-role.kubernetes.io/control-plane=
node-2 Ready <none> 10d v1.28.3 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=node-2,disktype=ssd,zone=us-west-2a
node-3 Ready <none> 10d v1.28.3 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=node-3,disktype=hdd,zone=us-west-2b 
 In this example, node-2 has the label disktype=ssd, which we can use for affinity.

<style>
.eno-quiz-widget{margin:2rem 0;padding:1.5rem;border-radius:12px;background:var(--eno-surface-lowest,#f7f7f8);border:1px solid var(--eno-border-soft,#e2e2e6);font-family:var(--eno-font-body,Inter,sans-serif)}
.eno-quiz-widget .eno-quiz-kicker{font-family:var(--eno-font-label,"JetBrains Mono",monospace);font-size:.75rem;letter-spacing:.05em;text-transform:uppercase;color:var(--eno-text-muted,#5f5f68);margin:0 0 .5rem}
.eno-quiz-widget .eno-quiz-question{font-family:var(--eno-font-heading,"Hanken Grotesk",sans-serif);font-size:1.0625rem;font-weight:600;margin:0 0 1rem;color:var(--eno-text-strong,#1a1a1f)}
.eno-quiz-widget .eno-quiz-options{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.5rem}
.eno-quiz-widget .eno-quiz-option{display:block;width:100%;min-height:44px;text-align:left;padding:.625rem .875rem;border-radius:8px;border:1.5px solid var(--eno-border-soft,#e2e2e6);background:#fff;font-size:.9375rem;cursor:pointer;transition:border-color var(--eno-motion-base,180ms ease-out),background var(--eno-motion-base,180ms ease-out)}
.eno-quiz-widget .eno-quiz-option:hover{border-color:var(--eno-primary,#0059bb)}
.eno-quiz-widget .eno-quiz-option:focus-visible{outline:none;box-shadow:0 0 0 3px rgb(0 89 187 / 0.15)}
.eno-quiz-widget .eno-quiz-option[aria-pressed="true"]{border-color:var(--eno-primary,#0059bb);background:rgb(0 89 187 / 0.06)}
.eno-quiz-widget .eno-quiz-option[data-correct="true"].eno-quiz-revealed{border-color:var(--eno-success,#17803a);background:rgb(23 128 58 / 0.08)}
.eno-quiz-widget .eno-quiz-option[data-correct="false"].eno-quiz-revealed.eno-quiz-was-selected{border-color:var(--eno-error,#ba1a1a);background:var(--eno-error-soft,#ffdad6)}
.eno-quiz-widget .eno-quiz-option-icon{display:inline-block;width:1.1em;margin-right:.4em;font-weight:700}
.eno-quiz-widget .eno-quiz-submit{margin-top:1rem;min-height:44px;padding:.5rem 1.25rem;border-radius:8px;border:none;background:var(--eno-primary,#0059bb);color:#fff;font-weight:600;font-size:.9375rem;cursor:pointer;transition:opacity var(--eno-motion-base,180ms ease-out)}
.eno-quiz-widget .eno-quiz-submit:disabled{opacity:.5;cursor:not-allowed}
.eno-quiz-widget .eno-quiz-submit:focus-visible{outline:none;box-shadow:0 0 0 3px rgb(0 89 187 / 0.15)}
.eno-quiz-widget .eno-quiz-explanation{margin-top:1rem;padding:.875rem 1rem;border-radius:8px;font-size:.9375rem;line-height:1.5;display:none}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-visible{display:block}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-correct{background:rgb(23 128 58 / 0.08);color:var(--eno-success,#17803a)}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-incorrect{background:var(--eno-error-soft,#ffdad6);color:var(--eno-error,#ba1a1a)}
@media (prefers-reduced-motion: reduce){.eno-quiz-widget *{transition:none!important}}
</style><div class="eno-quiz-widget" role="group" aria-label="Quick check question">
Quick check 1 of 2

What are the two types of node affinity defined in the reference passage?

- requiredDuringSchedulingIgnoredDuringExecution and preferredDuringSchedulingIgnoredDuringExecution
- hard and soft
- strict and lenient
- mandatory and optional

<button type="button" class="eno-quiz-submit" disabled>Submit</button>
<div class="eno-quiz-explanation">The passage explicitly lists two types: requiredDuringSchedulingIgnoredDuringExecution and preferredDuringSchedulingIgnoredDuringExecution.</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
  document.querySelectorAll('.eno-quiz-widget:not([data-eno-quiz-bound])').forEach(function (widget) {
    widget.setAttribute('data-eno-quiz-bound', '1');
    var options = Array.prototype.slice.call(widget.querySelectorAll('.eno-quiz-option'));
    var submitBtn = widget.querySelector('.eno-quiz-submit');
    var explanation = widget.querySelector('.eno-quiz-explanation');
    var selected = null;
    options.forEach(function (opt) {
      opt.addEventListener('click', function () {
        if (widget.hasAttribute('data-eno-quiz-answered')) return;
        options.forEach(function (o) { o.setAttribute('aria-pressed', 'false'); });
        opt.setAttribute('aria-pressed', 'true');
        selected = opt;
        submitBtn.disabled = false;
      });
    });
    submitBtn.addEventListener('click', function () {
      if (!selected || widget.hasAttribute('data-eno-quiz-answered')) return;
      widget.setAttribute('data-eno-quiz-answered', '1');
      submitBtn.disabled = true;
      var correct = selected.getAttribute('data-correct') === 'true';
      options.forEach(function (o) {
        o.classList.add('eno-quiz-revealed');
        if (o === selected) o.classList.add('eno-quiz-was-selected');
        var icon = o.querySelector('.eno-quiz-option-icon');
        if (o.getAttribute('data-correct') === 'true') icon.textContent = '\u2713';
        else if (o === selected) icon.textContent = '\u2717';
      });
      explanation.classList.add('eno-quiz-visible', correct ? 'eno-quiz-correct' : 'eno-quiz-incorrect');
    });
  });
});
</script>

## Safe Configuration Path

Start with a read-only observation of the current scheduling state. Then make the smallest change that demonstrates node affinity and verify it before expanding.

### Step 1: Observe existing pods and their node placement

Run:

kubectl get pods -o wide --all-namespaces 
 This shows where pods are currently running. If you have a specific namespace, use -n <namespace> .

### Step 2: Design a minimal node affinity manifest

Create a file named nginx-affinity.yaml :

apiVersion: v1
kind: Pod
metadata:
 name: nginx-affinity
spec:
 containers:
 - name: nginx
 image: nginx:1.25
 ports:
 - containerPort: 80
 affinity:
 nodeAffinity:
 requiredDuringSchedulingIgnoredDuringExecution:
 nodeSelectorTerms:
 - matchExpressions:
 - key: disktype
 operator: In
 values:
 - ssd 
 This pod requires a node with the label disktype=ssd . It will only be scheduled on node-2 in our example cluster.

### Step 3: Apply the manifest

kubectl apply -f nginx-affinity.yaml 
 Expected output:

pod/nginx-affinity created 

### Step 4: Verify scheduling

 Check the pod status and node:

kubectl get pod nginx-affinity -o wide 
 Example output:

NAME READY STATUS RESTARTS AGE IP NODE
nginx-affinity 1/1 Running 0 15s 10.244.2.5 node-2 
 The pod is running on node-2, which matches the label. If no node matches, the pod remains in Pending state. We will cover that in the failure modes section.

### Step 5: Inspect scheduling events

Use kubectl describe pod nginx-affinity to see events:

Events:
 Type Reason Age From Message
 ---- ------ ---- ---- -------
 Normal Scheduled 25s default-scheduler Successfully assigned default/nginx-affinity to node-2
 Normal Pulling 24s kubelet Pulling image "nginx:1.25"
 Normal Started 23s kubelet Started container nginx
 Normal Created 23s kubelet Created container nginx 
 The Scheduled event confirms the scheduler used the affinity rule.

## Verification and Diagnostics

Now that a basic required rule works, expand to preferred rules and combine affinity with other scheduling features.

### Preferred node affinity example

Create nginx-pref-affinity.yaml :

apiVersion: v1
kind: Pod
metadata:
 name: nginx-pref
spec:
 containers:
 - name: nginx
 image: nginx:1.25
 affinity:
 nodeAffinity:
 preferredDuringSchedulingIgnoredDuringExecution:
 - weight: 80
 preference:
 matchExpressions:
 - key: zone
 operator: In
 values:
 - us-west-2a
 - weight: 20
 preference:
 matchExpressions:
 - key: disktype
 operator: In
 values:
 - ssd 
 This pod prefers nodes in zone us-west-2a with a higher weight (80) and also prefers ssd nodes with weight 20. If no node matches, it still schedules on any node because there is no required rule.

Apply and verify:

kubectl apply -f nginx-pref-affinity.yaml
kubectl get pod nginx-pref -o wide 
 Example output:

NAME READY STATUS RESTARTS AGE IP NODE
nginx-pref 1/1 Running 0 10s 10.244.1.7 node-2 
 It runs on node-2, which matches both preferences. If node-2 were unavailable, it could run on node-1 or node-3.

### Using matchFields with node name

You can target a specific node using its name, though this is less flexible than labels. Example:

affinity:
 nodeAffinity:
 requiredDuringSchedulingIgnoredDuringExecution:
 nodeSelectorTerms:
 - matchFields:
 - key: metadata.name
 operator: In
 values:
 - node-2 
 This hard requirement forces the pod onto node-2. Use sparingly because it reduces scheduling resilience.

### Combining node affinity with resource requests

Add resource requests to influence scoring and prevent overcommit. For example, a memory-intensive pod:

apiVersion: v1
kind: Pod
metadata:
 name: mem-pod
spec:
 containers:
 - name: mem-container
 image: polinux/stress
 resources:
 requests:
 memory: "512Mi"
 cpu: "250m"
 affinity:
 nodeAffinity:
 requiredDuringSchedulingIgnoredDuringExecution:
 nodeSelectorTerms:
 - matchExpressions:
 - key: disktype
 operator: Exists 
 This pod requires a node with the disktype label (any value) and requests resources. The scheduler filters nodes by label and then by available capacity.

### Diagnostic commands for troubleshooting

When a pod with node affinity is pending, use these commands:

# Check pod events
kubectl describe pod <pod-name>

# Check node labels to see if any match
kubectl get nodes --show-labels

# Get scheduler logs (if you have access)
kubectl logs -n kube-system kube-scheduler-control-plane-1 | grep -i affinity 
 The scheduler logs may show why nodes were rejected. Look for messages like "node(s) didn't match node selector" or "0/3 nodes are available: 3 node(s) didn't match node affinity/selector." This is the standard event for failures.

<style>
.eno-quiz-widget{margin:2rem 0;padding:1.5rem;border-radius:12px;background:var(--eno-surface-lowest,#f7f7f8);border:1px solid var(--eno-border-soft,#e2e2e6);font-family:var(--eno-font-body,Inter,sans-serif)}
.eno-quiz-widget .eno-quiz-kicker{font-family:var(--eno-font-label,"JetBrains Mono",monospace);font-size:.75rem;letter-spacing:.05em;text-transform:uppercase;color:var(--eno-text-muted,#5f5f68);margin:0 0 .5rem}
.eno-quiz-widget .eno-quiz-question{font-family:var(--eno-font-heading,"Hanken Grotesk",sans-serif);font-size:1.0625rem;font-weight:600;margin:0 0 1rem;color:var(--eno-text-strong,#1a1a1f)}
.eno-quiz-widget .eno-quiz-options{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.5rem}
.eno-quiz-widget .eno-quiz-option{display:block;width:100%;min-height:44px;text-align:left;padding:.625rem .875rem;border-radius:8px;border:1.5px solid var(--eno-border-soft,#e2e2e6);background:#fff;font-size:.9375rem;cursor:pointer;transition:border-color var(--eno-motion-base,180ms ease-out),background var(--eno-motion-base,180ms ease-out)}
.eno-quiz-widget .eno-quiz-option:hover{border-color:var(--eno-primary,#0059bb)}
.eno-quiz-widget .eno-quiz-option:focus-visible{outline:none;box-shadow:0 0 0 3px rgb(0 89 187 / 0.15)}
.eno-quiz-widget .eno-quiz-option[aria-pressed="true"]{border-color:var(--eno-primary,#0059bb);background:rgb(0 89 187 / 0.06)}
.eno-quiz-widget .eno-quiz-option[data-correct="true"].eno-quiz-revealed{border-color:var(--eno-success,#17803a);background:rgb(23 128 58 / 0.08)}
.eno-quiz-widget .eno-quiz-option[data-correct="false"].eno-quiz-revealed.eno-quiz-was-selected{border-color:var(--eno-error,#ba1a1a);background:var(--eno-error-soft,#ffdad6)}
.eno-quiz-widget .eno-quiz-option-icon{display:inline-block;width:1.1em;margin-right:.4em;font-weight:700}
.eno-quiz-widget .eno-quiz-submit{margin-top:1rem;min-height:44px;padding:.5rem 1.25rem;border-radius:8px;border:none;background:var(--eno-primary,#0059bb);color:#fff;font-weight:600;font-size:.9375rem;cursor:pointer;transition:opacity var(--eno-motion-base,180ms ease-out)}
.eno-quiz-widget .eno-quiz-submit:disabled{opacity:.5;cursor:not-allowed}
.eno-quiz-widget .eno-quiz-submit:focus-visible{outline:none;box-shadow:0 0 0 3px rgb(0 89 187 / 0.15)}
.eno-quiz-widget .eno-quiz-explanation{margin-top:1rem;padding:.875rem 1rem;border-radius:8px;font-size:.9375rem;line-height:1.5;display:none}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-visible{display:block}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-correct{background:rgb(23 128 58 / 0.08);color:var(--eno-success,#17803a)}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-incorrect{background:var(--eno-error-soft,#ffdad6);color:var(--eno-error,#ba1a1a)}
@media (prefers-reduced-motion: reduce){.eno-quiz-widget *{transition:none!important}}
</style><div class="eno-quiz-widget" role="group" aria-label="Quick check question">
Quick check 2 of 2

According to the passage, what does 'IgnoredDuringExecution' mean in the context of node affinity?

- If node labels change after scheduling, the pod continues to run
- The rule is ignored during scheduling but enforced after
- The rule is only applied during execution
- If node labels change, the pod is rescheduled

<button type="button" class="eno-quiz-submit" disabled>Submit</button>
<div class="eno-quiz-explanation">The passage states: &#39;IgnoredDuringExecution means that if the node labels change after Kubernetes schedules the Pod, the Pod continues to run.&#39;</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
  document.querySelectorAll('.eno-quiz-widget:not([data-eno-quiz-bound])').forEach(function (widget) {
    widget.setAttribute('data-eno-quiz-bound', '1');
    var options = Array.prototype.slice.call(widget.querySelectorAll('.eno-quiz-option'));
    var submitBtn = widget.querySelector('.eno-quiz-submit');
    var explanation = widget.querySelector('.eno-quiz-explanation');
    var selected = null;
    options.forEach(function (opt) {
      opt.addEventListener('click', function () {
        if (widget.hasAttribute('data-eno-quiz-answered')) return;
        options.forEach(function (o) { o.setAttribute('aria-pressed', 'false'); });
        opt.setAttribute('aria-pressed', 'true');
        selected = opt;
        submitBtn.disabled = false;
      });
    });
    submitBtn.addEventListener('click', function () {
      if (!selected || widget.hasAttribute('data-eno-quiz-answered')) return;
      widget.setAttribute('data-eno-quiz-answered', '1');
      submitBtn.disabled = true;
      var correct = selected.getAttribute('data-correct') === 'true';
      options.forEach(function (o) {
        o.classList.add('eno-quiz-revealed');
        if (o === selected) o.classList.add('eno-quiz-was-selected');
        var icon = o.querySelector('.eno-quiz-option-icon');
        if (o.getAttribute('data-correct') === 'true') icon.textContent = '\u2713';
        else if (o === selected) icon.textContent = '\u2717';
      });
      explanation.classList.add('eno-quiz-visible', correct ? 'eno-quiz-correct' : 'eno-quiz-incorrect');
    });
  });
});
</script>

## Failure Modes and Recovery

Node affinity can cause pods to remain pending if requirements are too strict or labels are misconfigured. Here are common failure scenarios and how to recover.

### Scenario 1: No node matches required affinity

Suppose you apply a pod requiring disktype=nvme , but no node has that label. The pod stays Pending.

Check:

kubectl get pod nvme-pod
kubectl describe pod nvme-pod 
 The describe output includes:

Events:
 Type Reason Age From Message
 ---- ------ ---- ---- -------
 Warning FailedScheduling 12s default-scheduler 0/3 nodes are available: 3 node(s) didn't match node affinity/selector. preemption: 0/3 nodes are available: 3 Preemption is not helpful for scheduling. 
 Recovery options:

- Check if the label is missing or misspelled. Add the label to a node:

kubectl label node node-3 disktype=nvme 

- Modify the pod spec to use a softer preference or remove the requirement.

- If the pod is part of a Deployment, edit the deployment to fix the affinity, then rollout restart:

 kubectl edit deployment <deployment-name>
# change affinity, save and exit
kubectl rollout status deployment/<deployment-name> 

### Scenario 2: Affinity conflicts with taints

 A node may have the required label but also a taint that the pod does not tolerate. Example: node-2 has taint dedicated=special:NoSchedule . The pod with affinity for node-2 but no tolerance will not schedule.

Diagnose:

kubectl get node node-2 -o json | jq '.spec.taints' 
 Example output:

[
 {
 "effect": "NoSchedule",
 "key": "dedicated",
 "value": "special"
 }
] 
 Recovery:

- Add a toleration to the pod spec:

tolerations:
- key: dedicated
 operator: Equal
 value: special
 effect: NoSchedule 

- Or remove the taint if it is not needed:

 kubectl taint nodes node-2 dedicated=special:NoSchedule- 

### Scenario 3: Pod stuck terminating or unschedulable after node loss

 If a node that matches affinity goes down, pods with required affinity to that node will not reschedule elsewhere because no other node satisfies the rule. Use kubectl get pods -o wide to see node status. If the node is NotReady, the pods may be stuck in Terminating or Pending.

Recovery:

- Evict the pod manually:

kubectl delete pod <pod-name> --force --grace-period=0 

- Then either add the missing label to another node or change the affinity to a preferred rule.

## Operations Checklist

 Use this checklist before and after making node affinity changes in production.

### Before applying changes

- [ ] Run kubectl get nodes --show-labels and record the current labels of candidate nodes.

- [ ] Run kubectl get pods -o wide in the target namespace to see existing placement.

- [ ] Check if the scheduler is healthy: kubectl get pods -n kube-system -l component=kube-scheduler

- [ ] Review taints on nodes that should match: kubectl describe nodes | grep Taints

- [ ] Decide if you need required or preferred affinity. Prefer preferred for high availability unless you have a strict hardware requirement.

- [ ] Write the YAML manifest and validate it with kubectl apply --dry-run=client -f manifest.yaml

### After applying changes

- [ ] Verify pods are scheduled: kubectl get pods -o wide

- [ ] Check events for FailedScheduling warnings: kubectl describe pod - [ ] If using a Deployment, confirm rollout success: kubectl rollout status deployment/ 

- [ ] Test behavior by cordoning a matching node and seeing if pods reschedule according to affinity rules (if preferred) or stay pending (if required).

- [ ] Document the change in your runbook with the exact labels and affinity terms used.

### Quick reference for common affinity operators

 Operator Meaning Example 
 In Label value is in the list disktype In [ssd, nvme] 
 NotIn Label value is not in the list zone NotIn [us-west-2b] 
 Exists Label key exists regardless of value disktype Exists 
 DoesNotExist Label key does not exist gpu DoesNotExist 
 Gt Label value is greater than integer (for node affinity with matchFields? Not supported for label values) cpu-count Gt 4 (only in node selector? Not for affinity) 
 Lt Label value is less than integer Similar to above 

 Note: The Gt and Lt operators are not valid for label-based matchExpressions in node affinity; they are used in node selector terms with matchFields for metadata.name? Actually, they are not supported at all in node affinity. Stick to In, NotIn, Exists, DoesNotExist.

## Conclusion

Node affinity gives you fine-grained control over pod placement while maintaining clarity in your manifests. By using labels, match expressions, and weights, you can build scheduling policies that reflect your infrastructure topology and application requirements.

Always start with a small test, verify scheduling events, and prefer soft rules unless a hard constraint is necessary. Combine node affinity with tolerations, resource requests, and topology spread constraints to create resilient, efficient deployments.

To deepen your understanding, experiment with different match expressions, practice with taints and tolerations, and monitor scheduler behavior using the metrics endpoint. Node affinity is a foundational tool in the Kubernetes scheduler, and mastering it will make you more effective at managing production clusters.