Node Selector
spec.nodeSelectoris the simplest pod-to-node placement constraint in Kubernetes: a flat map of label key/value pairs that the scheduler treats as a hard requirement — the Pod is feasible only on nodes that carry every one of those labels with exactly those string values (Assigning Pods to Nodes). It is the oldest and least expressive of the placement mechanisms: equality-only (no “in a set”, no “exists”, no “greater than”) and AND-only (every pair must match; there is no OR). For anything beyond “this Pod needs an SSD node” it has been functionally superseded by Node Affinity, which expresses the same idea plus set membership, soft preferences, and disjunction.nodeSelectorsurvives because for the trivial case it is unbeatably terse — and because both can coexist on one Pod, in which case both must be satisfied (they are ANDed, never treated as alternatives).
Mental Model
nodeSelector is a set-membership filter expressed as a label dictionary. Picture every node carrying a bag of key=value labels; the Pod’s nodeSelector is a smaller bag; the node is a candidate iff the Pod’s bag is a subset of the node’s bag. There is no scoring, no preference, no fallback — a node either has all the labels or it does not.
flowchart TD POD["Pod<br/>nodeSelector:<br/>disktype: ssd<br/>gpu: 'true'"] --> SCHED[kube-scheduler<br/>NodeAffinity Filter plugin] N1["Node A<br/>disktype=ssd<br/>gpu=true"] -->|subset match| SCHED N2["Node B<br/>disktype=ssd"] -->|missing 'gpu' key| SCHED N3["Node C<br/>disktype=hdd<br/>gpu=true"] -->|wrong 'disktype' value| SCHED SCHED -->|feasible| N1 SCHED -.->|rejected| N2 SCHED -.->|rejected| N3
What this diagram shows and the insight to extract. Three nodes, one Pod requiring disktype=ssd AND gpu=true. Only Node A’s label bag is a superset of the Pod’s request. Node B is rejected for a missing key; Node C for a wrong value. The insight: nodeSelector is pure conjunction over exact equality — there is no way to say “ssd OR nvme” or “any node with a gpu label regardless of value.” The instant you need that, you have outgrown nodeSelector and want Node Affinity. Note also that the constraint is evaluated by the same NodeAffinity Filter plugin that handles node affinity — nodeSelector is, mechanically, a degenerate special case of node affinity inside the scheduler, and they share a code path and the same FailedScheduling event text.
Mechanical Walk-through
A node carries labels two ways: automatic well-known labels the kubelet and cloud-controller-manager populate, and operator-applied labels set with kubectl label nodes. When a Pod with a nodeSelector enters the scheduler, the NodeAffinity Filter plugin (see Scheduling Framework) reads the Pod’s spec.nodeSelector map and, for each candidate node, checks that every key is present with a matching value. A single mismatch rejects the node — it never reaches the Score phase. If no node matches, the Pod stays Pending with a FailedScheduling event of the form 0/N nodes are available: N node(s) didn't match Pod's node affinity/selector.
The crucial well-known node labels usable as nodeSelector keys (Well-Known Labels reference):
kubernetes.io/hostname— the node’s name; pin a Pod to one specific node (thoughspec.nodeNameis the blunter tool for a literal pin).kubernetes.io/os—linuxorwindows; the canonical guard for an OS-specific workload. A Linux container scheduled onto a Windows node simply fails to start, so every workload in a mixed-OS cluster should set this.kubernetes.io/arch—amd64,arm64, etc.; pin a single-arch image to a compatible CPU architecture in a mixed-arch (e.g. AWS Graviton arm64 alongside x86) cluster.topology.kubernetes.io/zone— the cloud availability zone (e.g.us-east-1a).topology.kubernetes.io/region— the cloud region (e.g.us-east-1).node.kubernetes.io/instance-type— the cloud instance type (e.g.m6i.4xlarge); steer a memory-hungry Pod to large instances.node.kubernetes.io/windows-build— the Windows OS build, on Windows nodes only; matters because Windows containers require host/container build parity.
The legacy beta.kubernetes.io/arch, beta.kubernetes.io/os, beta.kubernetes.io/instance-type, and failure-domain.beta.kubernetes.io/{zone,region} labels are deprecated in favor of the non-beta forms above; older manifests still reference them but new work should not.
Operator-applied labels are arbitrary: disktype=ssd, gpu=true, team=payments. A label is set with kubectl label nodes worker-1 disktype=ssd and removed with the trailing-dash syntax disktype-.
nodeSelector and DaemonSets. A DaemonSet with a .spec.template.spec.nodeSelector runs its Pod only on matching nodes; with none, it runs on every node (DaemonSet docs). This is how a monitoring agent or CNI Pod is restricted to (say) Linux nodes. Internally the DaemonSet controller translates the eligible-node set into per-Pod node affinity pinning each Pod to one node, and the default scheduler then binds — so even DaemonSet placement flows through the same NodeAffinity plugin.
Scheduling-time only. nodeSelector is evaluated only at scheduling time. If a node’s label changes after the Pod is bound, the running Pod is not evicted — the same “ignored during execution” semantics that Node Affinity’s name makes explicit. There is deliberately no controller that watches node labels and evicts mismatched running Pods; when you need eviction-on-state-change, the tool is Taints and Tolerations with the NoExecute effect.
Configuration / API Surface
apiVersion: apps/v1
kind: Deployment
metadata:
name: ssd-cache
spec:
replicas: 3
selector:
matchLabels:
app: ssd-cache
template:
metadata:
labels:
app: ssd-cache
spec:
nodeSelector: # the entire placement constraint
disktype: ssd # operator-applied label
kubernetes.io/arch: amd64 # well-known label — exclude arm64 nodes
node.kubernetes.io/instance-type: m6i.2xlarge # pin to one instance type
containers:
- name: cache
image: redis:7Line-by-line. nodeSelector lives directly under the Pod spec (here inside the Deployment’s template.spec). Each map entry is an independent AND clause: the node must satisfy disktype=ssd and kubernetes.io/arch=amd64 and node.kubernetes.io/instance-type=m6i.2xlarge. There is no syntax for “or”, for “any value”, or for “not equal” — those require Node Affinity. The values are compared as strings; even a boolean-looking value such as gpu: "true" must be quoted, because unquoted YAML would parse true as a boolean and the label comparison would mismatch the stored string "true".
Apply a matching node label so the Deployment can schedule:
kubectl label nodes worker-3 disktype=ssd # add the label
kubectl get nodes -l disktype=ssd # list nodes that match
kubectl get nodes --show-labels # see every label on every node
kubectl label nodes worker-3 disktype- # remove it (trailing dash)If nodeSelector and affinity.nodeAffinity are both set on a Pod, the scheduler requires both — a frequent surprise that produces a stricter constraint than the author intended.
Failure Modes
- Typo in a label key or value.
disktype: sdd(transposed) matches nothing; the Pod staysPendingforever with0/N nodes are available: N node(s) didn't match Pod's node affinity/selector. Diagnose by diffingkubectl get nodes --show-labelsagainst the Pod’sspec.nodeSelectorfromkubectl get pod -o yaml. - Label drift. A node loses a label (a re-image, a tooling bug, a cloud-controller change) and new Pods can no longer schedule there, while existing Pods keep running — a confusing partial outage. The asymmetry (scheduled Pods unaffected, new ones stuck) is the tell.
- Over-constraining into a single point of failure. A
nodeSelectorso specific it matches one node funnels every replica onto that node; the node dies and the whole Deployment goes down.nodeSelectorexpresses requirement, never spread — pair it with Topology Spread Constraints for HA. - Reserved-prefix labels rejected. Labels under
kubernetes.io/andk8s.io/are reserved; a kubelet self-applying one (or one undernode-restriction.kubernetes.io/) can be blocked by theNodeRestrictionadmission plugin. Use your own domain prefix (example.com/disktype) for custom labels. nodeSelectorplusnodeAffinityover-constraint. Both must hold; authors who think one overrides the other create an unschedulable Pod. Pick one mechanism per intent.
Alternatives and When to Choose Them
| Mechanism | Expressiveness | Hard / Soft | When to use |
|---|---|---|---|
nodeSelector | equality + AND only | hard | trivial “must have label X” cases |
| Node Affinity | set ops (In/NotIn/Exists/Gt/Lt), AND + OR | hard and soft | anything beyond exact equality; preferences |
| Taints and Tolerations | node repels Pods by default | hard, incl. NoExecute eviction | dedicating nodes; the inverse — opt-out not opt-in |
| Topology Spread Constraints | even distribution across domains | soft or hard | HA spreading, which nodeSelector cannot do |
spec.nodeName | exact node, bypasses the scheduler entirely | hard | static Pods, debugging — almost never in production |
Honest “when nodeSelector is still fine.” Despite node affinity being a strict superset, nodeSelector remains the right tool when all of: the constraint is one-to-three exact-match labels; it is a hard requirement (no preference, no fallback); and the manifest is read by humans who benefit from the terseness. Examples: kubernetes.io/os: linux on a Linux-only DaemonSet, or a single disktype: ssd. The verbosity of nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[].matchExpressions[] to express the identical thing is not worth it. The moment you want “in a set”, “exists”, a soft preference, or disjunction, switch to Node Affinity — it is the official recommendation and a clean superset.
Production Notes
- Mixed-architecture clusters (AWS Graviton arm64 alongside x86
amd64) are the most common modernnodeSelectoruse:kubernetes.io/archpins a single-arch image to compatible nodes. Multi-arch container images make even this unnecessary, butnodeSelectoris the fallback for images built for only one architecture. - Windows + Linux clusters universally set
kubernetes.io/oson every workload; omitting it lets a Linux container be scheduled onto a Windows node, where it fails to start with an opaque runtime error. - Managed node pools. GKE, EKS, and AKS each expose their pool identity as a node label —
cloud.google.com/gke-nodepoolon GKE,eks.amazonaws.com/nodegroupon EKS,agentpool(orkubernetes.azure.com/agentpool) on AKS — and steering Pods to a specific pool vianodeSelectoris the standard pattern. Node affinity is increasingly preferred for the same purpose because itspreferredform degrades gracefully when a pool is full, whereasnodeSelectorsimply leaves the PodPending. - Anti-pattern: encoding HA into
nodeSelector. Teams sometimes expect anodeSelectorto spread replicas and are surprised when all replicas pile onto one matching node.nodeSelectoris a placement filter, never a spreader; HA requires Topology Spread Constraints or Pod Affinity and Anti-Affinity. - Security-relevant labels should use the
node-restriction.kubernetes.io/prefix so a compromised kubelet cannot relabel its own node into a sensitive workload’s feasible set — relevant whennodeSelectorgates placement of privileged workloads.
See Also
- Node Affinity — the expressive successor;
nodeSelectoris its degenerate case - Taints and Tolerations — the inverse mechanism; nodes repel rather than attract
- Topology Spread Constraints — for the HA-spread case
nodeSelectorcannot express - Pod Affinity and Anti-Affinity — placement relative to other Pods, not node labels
- Kubernetes Labels and Selectors — the label/selector machinery underneath
- Kubernetes Scheduler Architecture — where the
NodeAffinityFilter plugin runs - Scheduling Framework — the plugin that evaluates
nodeSelector - DaemonSet — uses
nodeSelectorto restrict per-node Pods - kube-scheduler — the scheduler component
- Kubernetes MOC — parent index