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 [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. 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 [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 owns that side of the story.