Kubernetes topology spread constraints influence how Pods are placed across eligible nodes or zones. Use DoNotSchedule when violating the configured skew should keep a Pod Pending. ScheduleAnyway is a preference and does not guarantee an even distribution.

Create a disposable three-worker lab

Install Docker, kind 0.33.0 and a kubectl client compatible with Kubernetes 1.37. This local lab uses one control-plane node and three workers. It consumes laptop resources and does not represent three independent physical failure domains. Use the dedicated kubeconfig for every command below.

cat > kind-spread.yaml <<'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
  - role: worker
  - role: worker
  - role: worker
EOF
kind create cluster --name cloudhero-spread-lab \
  --config kind-spread.yaml --kubeconfig ./spread.kubeconfig \
  --image kindest/node:v1.37.0@sha256:a1ed56cfb0e7b93589bdf97c8cd566405a265939e3620fc4f5de89adff580ae5
export KUBECONFIG="$PWD/spread.kubeconfig"
kubectl get nodes -L kubernetes.io/hostname

Apply a hard spread constraint

Save this as spread.yaml. Normal kind worker labels are sufficient; do not replace their hostname files.

apiVersion: v1
kind: Namespace
metadata:
  name: spread-lab
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: spread
  namespace: spread-lab
spec:
  replicas: 6
  selector:
    matchLabels:
      app: spread
  template:
    metadata:
      labels:
        app: spread
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          minDomains: 3
          nodeTaintsPolicy: Honor
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: spread
      containers:
        - name: sleeper
          image: docker.io/library/busybox@sha256:a7dbc319b1438115a207e11930dc4bb3e643dcb3ec0bdd694bb36cd8fad289ec
          command: ["sleep", "3600"]
          resources:
            requests:
              cpu: 5m
              memory: 8Mi
            limits:
              memory: 32Mi
kubectl apply -f spread.yaml
kubectl -n spread-lab rollout status deployment/spread --timeout=120s
kubectl -n spread-lab get pods -l app=spread -o wide
kubectl -n spread-lab scale deployment/spread --replicas=9
kubectl -n spread-lab rollout status deployment/spread --timeout=120s

With three eligible workers and no conflicting constraints, six replicas should produce two per worker and nine should produce three. nodeTaintsPolicy: Honor excludes nodes with untolerated taints from the domain calculation, including this lab’s control plane. Inspect the actual placement; scheduling also depends on requests, taints and affinity.

Test insufficient eligible domains

minDomains: 3 makes missing domains visible. In a separate one-worker lab, the hard rule permits only a limited number of matching Pods before the remainder stay Pending. Read their scheduling events. To test the soft alternative, remove minDomains and use ScheduleAnyway; do not keep the hard-only field with a soft rule.

What happens during scale-in?

These constraints guide placement of incoming Pods. They do not continuously rebalance existing Pods or guarantee even placement after a scale-down. Zone availability also depends on correct zone labels and suitable capacity. Evaluate disruption and recovery separately.

Delete the lab

kind delete cluster --name cloudhero-spread-lab --kubeconfig ./spread.kubeconfig

Sources and further reading