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 [Affinity and anti-affinity](/docs/hetzner/apalla/workloads/placement/affinity-and-anti-affinity) | | Replicas on separate nodes, so one drain takes at most one | **Topology spread constraints**. See [Spread pods with topology constraints](/docs/hetzner/apalla/workloads/placement/topology-spread-constraints) | | A pod near or away from specific other pods | **Pod affinity / anti-affinity**. See [Affinity and anti-affinity](/docs/hetzner/apalla/workloads/placement/affinity-and-anti-affinity) | | Nodes dedicated to one workload | A **dedicated node pool** with a label. See [Reserve nodes with taints and tolerations](/docs/hetzner/apalla/workloads/placement/taints-and-tolerations) | | 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 [Reserve nodes with taints and tolerations](/docs/hetzner/apalla/workloads/placement/taints-and-tolerations) 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 ([Spread pods with topology constraints](/docs/hetzner/apalla/workloads/placement/topology-spread-constraints)). 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 [Node labels and annotations](/docs/hetzner/apalla/reference/node-labels-and-annotations). ## 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 [Spread nodes with placement groups](/docs/hetzner/apalla/servers-and-nodes/provision/placement-groups) and the [cluster variables reference](/docs/hetzner/apalla/reference/cluster-variables). > [!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 [PodDisruptionBudget](/docs/hetzner/apalla/workloads/production/pod-disruption-budgets), which controls that behavior.