Skip to main content
RunBook Academy

KubernetesXXIII · nodeSelector and Node AffinityNode affinity

Well-known node labels — kubernetes.io and topology.kubernetes.io

Advanced⏱ ~16 minkubectlkubeadm

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

Not yet marked complete on this device.

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

LabelSet byValues
kubernetes.io/hostnamekubeletnode’s hostname
kubernetes.io/archkubeletamd64, arm64, ppc64le, s390x
kubernetes.io/oskubeletlinux, windows
topology.kubernetes.io/zonecloud providerus-east-1a, eu-west-1b, etc.
topology.kubernetes.io/regioncloud providerus-east-1, eu-west-1, etc.
topology.kubernetes.io/zone (legacy)older kubeletssame as above
node.kubernetes.io/instance-typecloud providerm5.large, n1-standard-2, etc.
kubernetes.io/rolekubeadmcontrol-plane, worker (deprecated alias)
node-role.kubernetes.io/control-planekubeadm""
node-role.kubernetes.io/workerkubeadm""
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 class
  • tier=production|staging|development — workload class
  • gpu=true — hardware capability
  • dedicated=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

  1. Q1. Which label does the kubelet set automatically on every node?

  2. Q2. Manifests using topology.kubernetes.io/zone work across clusters because the label is set by all major cloud providers and on-premises distributions.

  3. 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.

  4. 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-labels to 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.