Home / Stories / Availability
Story · Reliability Live

Spot nodes get shot. Your service shouldn't notice.

Spot capacity is cheap because the provider can take a node back at any moment. Some teams even treat that as a free chaos monkey. It only works if every workload is laid out to survive losing a node. This is how the assistant finds the ones that are not, and what it proposes so the next preemption is a non-event.

Starts fromEvery Deployment, StatefulSet and HelmRelease in your repositories, once a day.
Looks atReplica counts, disruption budgets, spread constraints, grace periods and priority classes in git, plus the cluster's zones and spot pools.
You getSmall draft MRs from a deterministic rule pack: no language model involved, same input, same output.
01 · Know the cluster

Three zones, all spot.

The cluster-state scan records the shape of each cluster: how many availability zones it spans, whether it has spot nodes, and whether it runs only on spot. It reads node labels and nothing else.

Here: three zones, four spot nodes. checkout-api runs two replicas, but both landed on the same node. payments-operator and orders-worker run a single replica each, on that same node.

02 · The node gets taken back

One preemption, three outages.

The provider reclaims node a-1 with a few seconds of notice. Everything on it stops at once.

checkout-api had two replicas and still went fully dark: replicas that share a node share its fate. Until the scheduler finds room and the new pods pass their readiness checks, nothing serves checkout.

03 · Find the weak spots, before it happens

The rule pack reads the manifests that allowed it.

The same gaps are visible in git long before any node dies. The availability rules walk every workload daily and check it against the cluster it runs on:

  • multi-replica, but nothing spreads the replicas over nodes and zones,
  • multi-replica, but no PodDisruptionBudget,
  • a controller-manager running as a single replica,
  • a grace period longer than spot allows.
04 · No model in the loop

Deterministic rules, not a guess.

This agent does not call a language model at all. Each rule is a plain check with a fixed fix, so a finding can be reproduced and explained line by line, and it costs nothing to run every day.

Rules that need topology skip themselves when the zone count is unknown, rather than guessing. When no clean diff can be written, the finding becomes an issue instead of a risky MR.

05 · Spread the replicas

One replica per node, one per zone where possible.

On a multi-zone cluster, a multi-replica workload without topologySpreadConstraints gets two: one keyed on the node, one on the zone, each with maxSkew: 1 and the workload's own selector labels.

Both are ScheduleAnyway: a best-effort spread that can never leave a pod Pending when capacity is short. You can tighten it to DoNotSchedule if you have guaranteed room.

06 · Budget the disruptions

Drains and upgrades take one pod at a time.

A multi-replica workload with no PodDisruptionBudget gets one, in a new file next to the workload, wired into the directory's kustomization.

To be precise about what that buys: a PDB governs voluntary evictions, such as node drains, pool upgrades and autoscaler scale-downs. A spot preemption does not ask permission. That is what the spread in step 05 is for. The two cover different failures, so you get both.

07 · Controllers and grace periods

Leader-elected controllers get standbys. Grace fits spot.

A controller-manager running alone gets a proposal for 3 replicas: leader election means only one works at a time, the others take over when its node dies. It ships as a "judgment call" draft, and it is skipped for controllers whose standbys cannot pass readiness.

On a spot-only cluster, a terminationGracePeriodSeconds above 30 is trimmed to 30: spot never waits longer, and a long grace also slows every voluntary drain. Trimming further, to 15 s when every live pod is provably on spot, is In development.

08 · The same preemption, after merge

Node a-1 goes. Checkout keeps serving.

With the replicas spread, losing a-1 costs one checkout-api replica, not the service. The operator's standby in another zone takes the lease. The lost pods are rescheduled in the background.

orders-worker still has one replica, so it still blips. Proposing extra replicas for ordinary single-replica apps, with evidence that the app tolerates it, is Roadmap: some singletons are singletons on purpose.

zone a
a-1
ckckopow
a-2
··
zone b
b-1
···
zone c
c-1
··
ckcheckout-api ×2 oppayments-operator ×1 oworders-worker ×1 ·other workloads
topology: availability_zones=3 · spot_present=true · spot_only=true
zone a
a-1 · preempted
ckckopow
a-2
··ck
zone b
b-1
···ck
zone c
c-1
··opow
checkout-api 0/2 ready · payments-operator 0/1 · orders-worker 0/1 · pods rescheduling
availability rules · repo shop-deploy · cluster 3 AZ, spot-only
  • checkout-api · replicas 2 · no topologySpreadConstraints rule 6
  • checkout-api · replicas 2 · no PodDisruptionBudget rule 1
  • payments-operator · leader election · replicas 1 rule 2
  • orders-worker · terminationGracePeriodSeconds 120 on spot-only rule 3b
  • search-api · replicas 3 · spread + PDB present ok
  • orders-worker · replicas 1 no rule today
4 findings · 4 draft MRs · 0 model calls
Manifestsgit, parsed
Topologyzones · spot
Rule packzero LLM
Draft MRor issue
rule 6 · what it checks
if kind in (Deployment, StatefulSet, HelmRelease)
   and replicas ≥ 2
   and not has_topology_spread(workload)
   and cluster.availability_zones ≥ 2:     # unknown → skip
    propose(spread_by_host_and_zone(workload))  # no clean diff → issue
apps/shop/checkout-api/deployment.yaml+14
  template:
    spec:
+     topologySpreadConstraints:
+       - maxSkew: 1
+         topologyKey: kubernetes.io/hostname
+         whenUnsatisfiable: ScheduleAnyway
+         labelSelector:
+           matchLabels:
+             app.kubernetes.io/name: checkout-api
+       - maxSkew: 1
+         topologyKey: topology.kubernetes.io/zone
+         whenUnsatisfiable: ScheduleAnyway
+         labelSelector:
+           matchLabels:
+             app.kubernetes.io/name: checkout-api
      containers:
        - name: api
category:reliabilitysource:availability_rulesrisk:low
apps/shop/checkout-api/poddisruptionbudget_checkout-api.yamlnew file
+apiVersion: policy/v1
+kind: PodDisruptionBudget
+metadata:
+  name: checkout-api-pdb
+  namespace: shop
+spec:
+  minAvailable: 1
+  selector:
+    matchLabels:
+      app.kubernetes.io/name: checkout-api
apps/shop/checkout-api/kustomization.yaml
  resources:
    - deployment.yaml
    - service.yaml
+   - poddisruptionbudget_checkout-api.yaml
covers drains, pool upgrades, scale-downs · does not stop a preemption
apps/payments/payments-operator/deployment.yaml
  spec:
-   replicas: 1
+   replicas: 3
    template:
      spec:
        containers:
          - args: ["--leader-elect"]   # standbys wait for the lease
apps/shop/orders-worker/deployment.yaml
      spec:
-       terminationGracePeriodSeconds: 120
+       terminationGracePeriodSeconds: 30
zone a
a-1 · preempted
ckopow
a-2
··ck
zone b
b-1
··ckopop
zone c
c-1
·opow
checkout-api 1/2 serving → 2/2 · payments-operator lease moved to zone b
orders-worker 0/1 while rescheduling (single replica: its replica bump is a separate MR)
Illustrative replay. The assistant does not run chaos tests; it fixes the layout they would expose.

The merge request you get.

One rule, one small MR. This is the spread proposal for checkout-api; the PodDisruptionBudget arrives as its own MR so each can be reviewed and reverted on its own. Illustrative, shaped like the real output.

Draft

Set topologySpreadConstraints on multi-replica Deployment shop/checkout-api

from gitops-ai/topology-spread-checkout-api-7b2e into main · 1 file · +14
category:reliabilitysource:availability_rulesrisk:lowxrev::LGTM
OverviewCluster topologyChangesAfter merge
Summary

Deployment shop/checkout-api runs 2 replicas on a 3-zone cluster but declares no topologySpreadConstraints. Multiple replicas landing on the same node defeats the HA intent: a single-node failure takes all replicas down at once.

What this adds

Two soft constraints on the pod template: spread by kubernetes.io/hostname and by topology.kubernetes.io/zone, maxSkew: 1, selecting on the workload's own labels.

Why ScheduleAnyway

A hard DoNotSchedule can leave a replica Pending when the cluster cannot satisfy the skew, for example while a spot node is being replaced. Tighten it only if you have guaranteed node and zone capacity.

Related

A separate draft adds a PodDisruptionBudget (minAvailable: 1) for the same workload.

To opt out for this resource, add the skip annotation with the value availability.

SignalValueSource
Availability zones3node zone labels, last cluster scan
Spot nodes presentyesnode spot / preemptible labels
Spot onlyyesno on-demand pool
Effective replicas2spec.replicas in git
Existing spread / PDBnone / noneworkload and sibling files

No language model was used for this finding or this diff.

   template:
     spec:
+      topologySpreadConstraints:
+        - maxSkew: 1
+          topologyKey: kubernetes.io/hostname
+          whenUnsatisfiable: ScheduleAnyway
+          labelSelector:
+            matchLabels:
+              app.kubernetes.io/name: checkout-api
+        - maxSkew: 1
+          topologyKey: topology.kubernetes.io/zone
+          whenUnsatisfiable: ScheduleAnyway
+          labelSelector:
+            matchLabels:
+              app.kubernetes.io/name: checkout-api
       containers:
         - name: api
After merge, expect
  • Flux applies the Deployment; the pods roll once and are rescheduled against the new constraints.
  • kubectl get pods -n shop -l app.kubernetes.io/name=checkout-api -o wide shows the two replicas on different nodes, in different zones.
  • The next time a spot node that hosts one replica is reclaimed, the other keeps serving.
Rollback

Revert this MR. The constraints are soft, so they never block scheduling in the meantime.

R
independent reviewer second opinion, no pipeline context

Selector labels match the Deployment's pod template; constraints are soft; the cluster has three zones. LGTM.

What keeps it honest.

A rule that fires on the wrong workload is worse than no rule. Most of the work is in the cases where it stays quiet.

No topology, no zone ruleRules that depend on the number of zones skip entirely when it is unknown. They never assume three.
Chart-awareFor HelmReleases the fix goes into the chart's own values, and only when a render of the chart confirms the knob exists. Otherwise you get an issue with the recommended block.
Standbys that can stand byThe 3-replica controller rule is skipped for controllers whose standbys cannot pass readiness, which would only add crash-looping pods.
Evictions still possibleNo PDB diff is written whose budget would block every voluntary eviction at the current replica count; a stuck node drain is its own outage.
Fail-open on the live checkThe spot-aware 15 s grace only applies when every live pod is provably on spot. Mixed, on-demand or unreadable states keep the plain 30 s proposal.
Per-resource opt-outOne annotation on a workload silences the pack for it, for workloads that are laid out that way on purpose.

Where this stands.

Live
  • Zone and spot topology from the cluster scan
  • PodDisruptionBudgets for multi-replica workloads
  • Host and zone topology spread
  • 3 replicas for single controller-managers
  • Grace-period floor, and cap on spot-only clusters
  • priorityClassName for infra-critical pods on spot-only clusters
  • Readiness and liveness probes
  • Extra replicas for single-replica apps, proposed by the model-based repository scan
In development
  • Spot-aware 15 s grace when every live pod is on spot
  • Chart-value flips for spread, PDB, probes and replicas (render-confirmed, being measured)
  • Probe paths confirmed against the running pod
Roadmap
  • Replica proposals for singletons backed by deterministic evidence that the app tolerates more than one copy
← Previous storyFrom alert to a fix