Kubernetes Operations

Scheduling Controls: Node Selectors, Affinity, Taints, and Tolerations

The scheduler decides where Pods run. Scheduling controls let you influence placement without hardcoding infrastructure assumptions into application behavior.

What this lesson covers

The Kubernetes scheduler decides where Pods run. Scheduling controls let you influence that placement using node selectors, affinity, taints, tolerations, and topology spread.

  • How scheduling works at a high level
  • How node labels affect placement
  • How nodeSelector works
  • What node affinity adds
  • What taints and tolerations do
  • How to troubleshoot Pending Pods
Field note: Scheduling controls should express real placement needs, not random preference. Over-constraining Pods can make them impossible to schedule.

The short version

nodeSelector:
  Place Pods only on nodes with matching labels.

node affinity:
  More expressive node selection rules.

taints:
  Repel Pods from nodes.

tolerations:
  Allow Pods to tolerate matching taints.

topology spread:
  Distribute Pods across failure domains.

Before you start

kubectl config current-context
kubectl get nodes
kubectl create namespace scheduling-lab

Step 1: Inspect node labels

kubectl get nodes --show-labels

Nodes have labels such as operating system, architecture, hostname, and Kubernetes role. These labels can be used for scheduling.

Step 2: Add a training label to your kind node

kubectl label node stealth-k8s-lab-control-plane workload-type=training

Confirm:

kubectl get nodes --show-labels

Step 3: Create a Pod with nodeSelector

Create a file named:

nodeselector-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nodeselector-pod
  namespace: scheduling-lab
spec:
  nodeSelector:
    workload-type: training
  containers:
  - name: app
    image: nginx:latest
kubectl apply -f nodeselector-pod.yaml
kubectl get pods -n scheduling-lab -o wide

Step 4: Create an unschedulable nodeSelector Pod

Create a file named:

bad-nodeselector-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: bad-nodeselector-pod
  namespace: scheduling-lab
spec:
  nodeSelector:
    workload-type: does-not-exist
  containers:
  - name: app
    image: nginx:latest
kubectl apply -f bad-nodeselector-pod.yaml
kubectl get pods -n scheduling-lab
kubectl describe pod bad-nodeselector-pod -n scheduling-lab

The Pod should remain Pending because no node matches the selector.

Step 5: Create a Pod with node affinity

Create a file named:

node-affinity-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: node-affinity-pod
  namespace: scheduling-lab
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: workload-type
            operator: In
            values:
            - training
  containers:
  - name: app
    image: nginx:latest
kubectl apply -f node-affinity-pod.yaml
kubectl get pods -n scheduling-lab -o wide

Step 6: Understand taints and tolerations

A taint on a node says, “Do not schedule Pods here unless they tolerate this taint.”

In a kind cluster, the control-plane node may already have scheduling behavior suitable for labs. Be cautious adding taints to your only node because it can block new Pods.

Step 7: Add a temporary taint

kubectl taint node stealth-k8s-lab-control-plane dedicated=training:NoSchedule

Step 8: Create a Pod without a toleration

Create a file named:

no-toleration-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: no-toleration-pod
  namespace: scheduling-lab
spec:
  containers:
  - name: app
    image: nginx:latest
kubectl apply -f no-toleration-pod.yaml
kubectl get pods -n scheduling-lab
kubectl describe pod no-toleration-pod -n scheduling-lab

The Pod may remain Pending because it does not tolerate the taint.

Step 9: Create a Pod with a toleration

Create a file named:

toleration-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: toleration-pod
  namespace: scheduling-lab
spec:
  tolerations:
  - key: "dedicated"
    operator: "Equal"
    value: "training"
    effect: "NoSchedule"
  containers:
  - name: app
    image: nginx:latest
kubectl apply -f toleration-pod.yaml
kubectl get pods -n scheduling-lab -o wide

Step 10: Remove the taint

Remove the taint so your lab cluster returns to normal scheduling behavior:

kubectl taint node stealth-k8s-lab-control-plane dedicated=training:NoSchedule-

Step 11: Understand topology spread

Topology spread constraints help distribute Pods across failure domains such as zones, nodes, or racks. They matter more in multi-node and multi-zone clusters than in a one-node kind lab.

The mental model is simple: do not put all replicas in the same failure domain when you have better options.

Common scheduling troubleshooting commands

kubectl get pods -n <namespace>
kubectl describe pod <pod-name> -n <namespace>
kubectl get nodes --show-labels
kubectl describe node <node-name>
kubectl get events -n <namespace> --sort-by=.metadata.creationTimestamp

Common mistakes

  • Using node names instead of stable labels.
  • Adding constraints that no node satisfies.
  • Forgetting to remove a lab taint.
  • Assuming tolerations force placement. They only allow placement onto tainted nodes.
  • Using single-node lab behavior to infer multi-node production scheduling.

Step 12: Clean up

kubectl delete namespace scheduling-lab
kubectl label node stealth-k8s-lab-control-plane workload-type-
Bottom line: Scheduling controls influence where Pods run. Use labels, selectors, affinity, taints, and tolerations carefully so Pods land where they should without becoming impossible to schedule.

Check your understanding

  1. What does the scheduler decide?
  2. What does nodeSelector use?
  3. What does node affinity add beyond nodeSelector?
  4. What does a taint do?
  5. What does a toleration do?
  6. Does a toleration force a Pod onto a node?
  7. What command shows node labels?
  8. What command explains why a Pod is Pending?
  9. Why can over-constraining Pods be dangerous?
  10. What does topology spread try to improve?
Answer key
  1. Which node a Pod should run on.
  2. Node labels.
  3. More expressive matching rules and preferences.
  4. Repels Pods unless they tolerate it.
  5. Allows a Pod to be scheduled onto a node with a matching taint.
  6. No.
  7. kubectl get nodes --show-labels.
  8. kubectl describe pod <pod-name>.
  9. No node may satisfy all constraints, leaving Pods Pending.
  10. Distribution across failure domains.
← Previous: Jobs and CronJobs Series index Next: ServiceAccounts and RBAC →