KubernetesXXIII · nodeSelector and Node AffinityNode affinity
Well-known node labels — kubernetes.io and topology.kubernetes.io
What you'll learn
- List the well-known node labels and their values
- Use topology.kubernetes.io/zone and region for placement
- Reason about kubernetes.io/arch and os for image compatibility
- Distinguish well-known labels from custom labels
Prerequisites
Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16
Kubernetes and cloud providers set a standard set of node
labels automatically. These “well-known labels” are the
foundation of portable placement rules: a manifest with
topology.kubernetes.io/zone In [us-east-1a] works on any
cloud provider that sets the label correctly. This lesson
covers the well-known labels, their values, and how to use
them.
The labels
| Label | Set by | Values |
|---|---|---|
kubernetes.io/hostname | kubelet | node’s hostname |
kubernetes.io/arch | kubelet | amd64, arm64, ppc64le, s390x |
kubernetes.io/os | kubelet | linux, windows |
topology.kubernetes.io/zone | cloud provider | us-east-1a, eu-west-1b, etc. |
topology.kubernetes.io/region | cloud provider | us-east-1, eu-west-1, etc. |
topology.kubernetes.io/zone (legacy) | older kubelets | same as above |
node.kubernetes.io/instance-type | cloud provider | m5.large, n1-standard-2, etc. |
kubernetes.io/role | kubeadm | control-plane, worker (deprecated alias) |
node-role.kubernetes.io/control-plane | kubeadm | "" |
node-role.kubernetes.io/worker | kubeadm | "" |
node.kubernetes.io/lifecycle | (no longer set automatically) | (see below) |
flowchart TB
A[Node labels] --> B["kubernetes.io/*<br/>set by kubelet"]
A --> C["topology.kubernetes.io/*<br/>set by cloud"]
A --> D["node.kubernetes.io/*<br/>set by cloud or operator"]
A --> E["Custom labels<br/>set by operator"]
kubelet-set labels
kubernetes.io/hostname
kubectl get nodes -o custom-columns=NAME:.metadata.name,HOSTNAME:.metadata.labels.kubernetes\.io/hostname
# NAME HOSTNAME
# node-01 node-01
# node-02 node-02
The node’s hostname. Always set. Used in topologyKey
for pod anti-affinity.
kubernetes.io/arch and kubernetes.io/os
kubectl get nodes -o custom-columns=NAME:.metadata.name,ARCH:.metadata.labels.kubernetes\.io/arch,OS:.metadata.labels.kubernetes\.io/os
# NAME ARCH OS
# node-01 amd64 linux
# node-02 arm64 linux
The CPU architecture and operating system. Used to filter Pods by image compatibility — an ARM image cannot run on an amd64 node.
Cloud-provider labels
topology.kubernetes.io/zone
kubectl get nodes -o custom-columns=NAME:.metadata.name,ZONE:.metadata.labels.topology\.kubernetes\.io/zone
# NAME ZONE
# node-01 us-east-1a
# node-02 us-east-1b
# node-03 us-east-1a
The availability zone. Cloud providers set this automatically. Used for zone-pinning and zone-spreading.
topology.kubernetes.io/region
kubectl get nodes -o custom-columns=NAME:.metadata.name,REGION:.metadata.labels.topology\.kubernetes\.io/region
# NAME REGION
# node-01 us-east-1
# node-02 us-east-1
# node-03 us-east-1
The region. Used for region-pinning (rare; most workloads are zone-pinned).
node.kubernetes.io/instance-type
kubectl get nodes -o custom-columns=NAME:.metadata.name,TYPE:.metadata.labels.node\.kubernetes\.io/instance-type
# NAME TYPE
# node-01 m5.4xlarge
# node-02 m5.large
# node-03 g5.12xlarge
The cloud-provider instance type. Used to filter by hardware capability (GPU, memory, compute).
node.kubernetes.io/lifecycle
Note: in Kubernetes 1.34, this label is no longer set automatically. The taint
node.kubernetes.io/lifecycle=spot:PreferNoSchedule(or similar) is set by the cloud provider on spot instances.
# Legacy usage (may not work on modern clusters)
kubectl get nodes -o custom-columns=NAME:.metadata.name,LIFECYCLE:.metadata.labels.node\.kubernetes\.io/lifecycle
# NAME LIFECYCLE
# spot-01 spot
# ondemand-01 ondemand
Role labels (kubeadm)
kubectl get nodes -o custom-columns=NAME:.metadata.name,ROLES:.metadata.labels.node-role\.kubernetes\.io/control-plane,WORKER:.metadata.labels.node-role\.kubernetes\.io/worker
# NAME ROLES WORKER
# cp-01 <none> <none>
# worker-01 <none> ""
node-role.kubernetes.io/control-plane and
node-role.kubernetes.io/worker are set by kubeadm.
kubernetes.io/role is the legacy single-label form.
Custom labels
Operators can set any custom label:
kubectl label node node-01 disk=ssd
kubectl label node node-01 tier=production --overwrite
Custom labels are not part of the Kubernetes spec; the cluster’s operators must manage them. Common custom labels in production:
disk=ssd|hdd|nvme— storage classtier=production|staging|development— workload classgpu=true— hardware capabilitydedicated=tenant-name— multi-tenancy isolation
Inspection
# All labels on a node
kubectl get node node-01 -o jsonpath='{.metadata.labels}' | jq
# All labels across the cluster
kubectl get nodes -o json | jq '.items[].metadata.labels'
# Group nodes by zone
kubectl get nodes -o custom-columns=NAME:.metadata.name,ZONE:.metadata.labels.topology\.kubernetes\.io/zone
The portability rule
Manifests that use well-known labels are portable across
clusters. A Pod with topology.kubernetes.io/zone In [us-east-1a] works on EKS, GKE, AKS, and on-prem — as
long as the cluster’s nodes have the label.
Manifests that use custom labels are not portable. A Pod
with disk=ssd requires the operator to add disk=ssd to
the cluster’s nodes. Across clusters, the manifest needs
adjustment.
flowchart LR
A["Well-known labels<br/>topology.kubernetes.io/zone"] --> B["Portable<br/>across clusters"]
C["Custom labels<br/>disk=ssd"] --> D["Cluster-specific<br/>operator must set"]
Production patterns
Pattern 1: zone-aware spread
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: web
The cluster’s zones are the spreading domain. (See the topology-spread lesson for details.)
Pattern 2: zone pinning with failover
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values: ["us-east-1a"]
Prefer zone us-east-1a; fall back to other zones if no
nodes are available.
Pattern 3: ARM nodes
nodeSelector:
kubernetes.io/arch: arm64
A workload built for ARM. The image must also be ARM.
Pattern 4: GPU nodes
nodeSelector:
node.kubernetes.io/instance-type: g5.12xlarge
A workload that needs an instance with GPU. The container
must request nvidia.com/gpu.
Failure modes
Failure 1: missing label
# A node has no zone label
kubectl get node on-prem-01 -o jsonpath='{.metadata.labels}' | jq
# {} (empty)
A Pod with topology.kubernetes.io/zone In [us-east-1a]
does not match the on-prem node. The Pod is unschedulable
on that node. The fix: add the label or use a Node
Affinity that does not require a zone.
Failure 2: legacy label
# Legacy: failure-domain.beta.kubernetes.io/zone
# Modern: topology.kubernetes.io/zone
The failure-domain.beta.kubernetes.io/* labels were
deprecated in Kubernetes 1.17 and removed in 1.25+.
Manifests that use the legacy labels silently fail on
modern clusters.
# Verify the cluster has the modern labels
kubectl get nodes -o json | jq -r '.items[].metadata.labels | keys[]' | grep topology
# topology.kubernetes.io/zone
# topology.kubernetes.io/region
Failure 3: cloud-provider mismatch
A manifest written for AWS uses topology.kubernetes.io/ zone: us-east-1a. On a GKE cluster, the value is
us-central1-a. The Pod does not match. The fix: parameterise
the zone or use a generic selector.
Quiz
Knowledge check · 4 questions
Q1. Which label does the kubelet set automatically on every node?
Q2. Manifests using topology.kubernetes.io/zone work across clusters because the label is set by all major cloud providers and on-premises distributions.
Q3. Your on-premises cluster's nodes do not have topology.kubernetes.io/zone labels. Your Pods require zone us-east-1a. Diagnose.
On-premises cluster with 10 nodes. None have the zone label. Pods require topology.kubernetes.io/zone In us-east-1a.
Q4. Why are manifests using well-known labels more portable than manifests using custom labels?
Passing score: 75%. Answers are checked in this browser.
Production discipline
- Use well-known labels for portable placement. A
manifest with
topology.kubernetes.io/zone In [us-east-1a]works across clusters with the label set. - Document custom labels. A custom label (
disk=ssd) must be set by an operator or automation; the manifest alone is not sufficient. - Avoid the legacy
failure-domain.beta.kubernetes.io/*labels. They were removed in Kubernetes 1.25+. - Verify the labels exist before applying the manifest. A node without the expected label is a Pending Pod waiting to happen.
- Use
kubectl get nodes --show-labelsto audit. A dashboard that lists nodes and their labels catches drift and missing labels.
Well-known labels are the foundation of portable placement. Operators who use them correctly have manifests that work across clusters; operators who rely on custom labels have manifests that need adjustment.