Skip to main content

Steer workload placement

Inspect 1.36

Most workloads need no placement rules at all: the cluster already spreads a controller's pods across nodes and zones on its own. So before you write a nodeSelector or a spread constraint, ensure you need one.

Normally you need one when a pod requires a specific kind of node it would otherwise miss, when replicas must survive the loss of a single machine, or when a workload requires dedicated hardware that nothing else should share. Everything else, the scheduler already handles. When one of those reasons applies, find your case in the table below and follow its link. Most controls have their own page; the two simplest, node selectors and placement groups, are covered later on this page.

Which control to use

You want Use
Pods only on a certain kind of node (GPU, bare metal, one zone) A node selector (below)
Prefer a kind of node, but run elsewhere when it is full Node affinity, soft rule. See
Replicas on separate nodes, so one drain takes at most one Topology spread constraints. See
A pod near or away from specific other pods Pod affinity / anti-affinity. See
Nodes dedicated to one workload A dedicated node pool with a label. See
Cloud VMs on separate physical hosts Placement groups (below)

Almost everything in that table lives on the pod and pulls it toward nodes; placement groups are the outlier, acting on the cloud VMs themselves rather than on any pod. Note what none of these offer: there is no reliable way to push other pods away from a node, because a custom node taint does not survive node replacement. If you need to keep all other pods off a node, none of these controls do it; see for what to use instead.

How the default spread already works

Any pod that defines no spread of its own inherits a default one, applied across two axes:

Axis Label maxSkew When it cannot spread
Nodes kubernetes.io/hostname 3 Schedule anyway
Zones topology.kubernetes.io/zone 5 Schedule anyway

Both axes are schedule-anyway, which is the important part: a pod is never left Pending by the default. That safety has a cost in strictness. The rule is loose enough that three replicas can still land on one node, so anything that must survive a single node loss needs a tighter constraint you set yourself ( ). The zone label is published by the CCM on every node, though the value on bare-metal (Robot) nodes is less specific than on cloud nodes.

Pin a pod with a node selector

A nodeSelector is the bluntest and simplest steering control. It restricts a pod to nodes that carry every label you list, and nothing more:

yaml
		spec:
  nodeSelector:
    instance.hetzner.cloud/is-root-server: "true" # bare-metal workers
	
yaml
		spec:
  nodeSelector:
    autopilot.syself.com/gpu: "true" # GPU nodes
	

If no node matches, the pod stays Pending rather than landing somewhere unsuitable, which is usually what you want from a hard requirement. For the full label set, see .

Spread cloud VMs across physical hosts

Topology spread keeps pods on separate nodes. It says nothing about where those nodes physically live, so two of them can still sit on the same Hetzner host, and one host failure would take both. Placement groups close that gap by keeping the cloud VMs themselves on separate physical servers. Set them through topology variables:

yaml
		spec:
  topology:
    variables:
      - name: hcloudPlacementGroups
        value:
          - name: control-plane-spread
            type: spread
      - name: controlPlanePlacementGroupNameHcloud
        value: control-plane-spread
	

See and the .

Tip

Placement decides where a pod lands, not how many replicas can leave at once when a node drains. Pair any rule here with a , which controls that behavior.