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, be honest about whether you need one. 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 has earned hardware that nothing else should share. Everything else, the scheduler already handles.

When you do have one of those reasons, treat this page as a switchboard. Find your case in the table, then follow the link to the control that fits. Most controls have a page of their own; the two simplest, node selectors and placement groups, are short enough to cover right here. Either way, you pick the right lever the first time instead of using affinity when a plain label would do.

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. Pod-level or not, notice 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 were hoping to fence a node off from everything else, that is the wrong lever; 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 owns that side of the story.