Introduction
The scheduler normally chooses any feasible node. Labels and affinity express workload preferences, while taints let nodes repel Pods that lack matching tolerations.
You will progressively apply these controls to the prepared node and use Pod events to distinguish a hard scheduling rule from a soft preference.
Use this mental model: labels describe nodes, selectors and affinity express what a Pod wants, taints express what a node rejects, and tolerations let a Pod pass a matching rejection rule. None of these mechanisms directly “moves” an already running Pod.
Label the Node for Placement
In this step, add descriptive labels and expose them as columns.
The labels are intentionally about workload placement rather than application identity. workload-zone=local represents location, while disk-class=fast represents a capability. Kubernetes does not validate their business meaning; your scheduling rules give them meaning.
cd /home/labex/project/pod-scheduling
kubectl create namespace practice-scheduling
kubectl label node labex-v135 workload-zone=local disk-class=fast
kubectl get nodes -L workload-zone,disk-class
Labels are facts used by selectors and affinity; they do not move existing Pods automatically unless a controller creates replacements.
The -L option adds one column per label key. Seeing local and fast in the node row confirms the scheduler can evaluate the rules used in later steps.
NAME STATUS ROLES ... VERSION WORKLOAD-ZONE DISK-CLASS
labex-v135 Ready control-plane ... v1.35.5 local fast
This also confirms the upgraded environment is running Kubernetes v1.35.5. The two custom columns come from the labels you just added.
Require a Node with nodeSelector
In this step, create a Pod with the simplest exact-match placement rule.
nodeSelector is a map of required equality matches. The Pod is eligible only on a node where every key has the requested value. This is appropriate for simple mandatory placement.
cd /home/labex/project/pod-scheduling
cat <<'EOF' > selected-pod.yaml
apiVersion: v1
kind: Pod
metadata: {name: selected-pod, namespace: practice-scheduling}
spec:
nodeSelector: {disk-class: fast}
containers:
- {name: worker, image: busybox:1.36, imagePullPolicy: IfNotPresent, command: ["sh", "-c", "while true; do sleep 30; done"]}
EOF
Apply the Pod, wait for readiness, and inspect the assigned node in the NODE column:
kubectl apply -f selected-pod.yaml
kubectl wait --for=condition=Ready pod/selected-pod -n practice-scheduling --timeout=120s
kubectl get pod selected-pod -n practice-scheduling -o wide
nodeSelector is mandatory: a Pod remains Pending when no node has every requested label.
The selected node should be labex-v135. Scheduling success proves that disk-class=fast on the Node matched the Pod's requirement.
pod/selected-pod condition met
NAME READY STATUS ... NODE
selected-pod 1/1 Running ... labex-v135
The NODE column is the result of scheduling; the nodeSelector in the manifest is the reason this node was eligible.
Compare Required and Preferred Affinity
In this step, create a Pod that requires the local zone and prefers fast disk.
Node affinity is more expressive than nodeSelector. The required section filters out nodes that do not match; the preferred section assigns a weight to matching nodes but does not make that preference mandatory.
The long field names describe timing: the rule is evaluated during scheduling and ignored during execution. If a label changes later, Kubernetes does not evict the running Pod solely because of this affinity rule.
cd /home/labex/project/pod-scheduling
cat <<'EOF' > affinity-pod.yaml
apiVersion: v1
kind: Pod
metadata: {name: affinity-pod, namespace: practice-scheduling}
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions: [{key: workload-zone, operator: In, values: [local]}]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions: [{key: disk-class, operator: In, values: [fast]}]
containers:
- {name: worker, image: busybox:1.36, imagePullPolicy: IfNotPresent, command: ["sh", "-c", "while true; do sleep 30; done"]}
EOF
Apply the Pod and wait for scheduling:
kubectl apply -f affinity-pod.yaml
kubectl wait --for=condition=Ready pod/affinity-pod -n practice-scheduling --timeout=120s
kubectl get pod affinity-pod -n practice-scheduling -o wide
Required rules filter feasible nodes; preferred rules score those that remain. IgnoredDuringExecution means later label changes do not evict a running Pod.
The resulting table again names labex-v135. On this one-node cluster, the required workload-zone=local rule decides feasibility; the preferred fast-disk rule demonstrates scoring syntax without creating a second node solely for comparison.
Use a Taint and Toleration
In this step, taint the node, observe an untolerated Pod remain Pending, then create an allowed Pod.
The taint has key practice-reserved, value true, and effect NoSchedule. That effect blocks new Pods without a matching toleration but does not evict Pods already running on the node.
kubectl taint node NODE key=value:effect attaches that rule to a node. The following kubectl run creates a standalone Pod with no toleration, and waiting for its PodScheduled condition gives the scheduler a bounded ten seconds to demonstrate the expected failure.
cd /home/labex/project/pod-scheduling
kubectl taint node labex-v135 practice-reserved=true:NoSchedule
kubectl run blocked-pod -n practice-scheduling --image=busybox:1.36 --image-pull-policy=IfNotPresent --restart=Never -- sh -c 'sleep 3600'
kubectl wait --for=condition=PodScheduled pod/blocked-pod -n practice-scheduling --timeout=10s || true
kubectl events -n practice-scheduling --for pod/blocked-pod
The wait timeout is expected and || true lets the lesson continue. The scheduling event should mention an untolerated taint, providing evidence for why the Pod is Pending.
error: timed out waiting for the condition on pods/blocked-pod
LAST SEEN TYPE REASON OBJECT MESSAGE
... Warning FailedScheduling Pod/blocked-pod 0/1 nodes are available: 1 node(s) had untolerated taint(s)...
Here, Pending is the correct experimental result. The scheduler evaluated the Pod but found no node it was permitted to use.
Create a matching toleration. A toleration permits scheduling past the taint; it does not select the node by itself.
cat <<'EOF' > tolerated-pod.yaml
apiVersion: v1
kind: Pod
metadata: {name: tolerated-pod, namespace: practice-scheduling}
spec:
nodeSelector: {workload-zone: local}
tolerations:
- {key: practice-reserved, operator: Equal, value: "true", effect: NoSchedule}
containers:
- {name: worker, image: busybox:1.36, imagePullPolicy: IfNotPresent, command: ["sh", "-c", "while true; do sleep 30; done"]}
EOF
kubectl apply -f tolerated-pod.yaml
kubectl wait --for=condition=Ready pod/tolerated-pod -n practice-scheduling --timeout=120s
kubectl delete pod blocked-pod -n practice-scheduling
The tolerated Pod also carries a nodeSelector, so one rule chooses the node and the other permits it. Deleting the intentionally blocked Pod leaves the namespace in a clean, healthy state.
pod/tolerated-pod condition met confirms that adding the matching toleration changed the outcome. The earlier Pods remain running because NoSchedule affects new placement; it does not evict already running Pods.
Summary
You used labels, nodeSelector, required and preferred affinity, scheduler events, taints, and tolerations to express placement intent and node admission independently.


