feat(k8s): PodDisruptionBudgets for single-replica stateful workloads #234

Merged
k.renner merged 1 commits from fix/issue-201 into master 2026-07-14 10:56:11 +00:00
Member

Fixes #201

Adds minAvailable: 1 PodDisruptionBudgets for the data-bearing single-replica StatefulSets that had zero eviction protection:

Workload Kind Namespace Selector
gitea StatefulSet apps name=gitea
woodpecker-server StatefulSet apps name=woodpecker, component=server
protonmail-bridge StatefulSet paperless name=protonmail-bridge
syncthing StatefulSet paperless name=syncthing

On a 1-replica workload, minAvailable: 1 makes the eviction API refuse voluntary evictions, so kubectl drain / node maintenance blocks and surfaces the pod instead of yanking a live SQLite DB or RWO-PVC pod mid-write. Involuntary disruptions (node crash, OOM) are unaffected — as intended.

The woodpecker selector deliberately includes component: server so it does not also cover the stateless woodpecker agents.

Deliberately excluded (with rationale)

  • CNPG im-pg and gf-pg (instances:1) — CloudNativePG intentionally creates no primary PDB for single-instance clusters (no failover target) and handles graceful primary shutdown itself (smartShutdownTimeout/stopDelay, already tuned on im-pg). A manual maxUnavailable: 0 would only wedge node drains with no HA benefit and fight the operator design. im-pg is single-instance only during the pgvecto.rs→VectorChord migration window.
  • Ceph mons — the Rook operator self-manages quorum-preserving mon/OSD PDBs (disruptionManagement.managePodBudgets, default-on; no override in this repo). A hand-written PDB would be redundant or overwritten each reconcile.
  • Stateless single-replica Deployments (paperless redis/tika/gotenberg, whisper, etc.) — a blocking PDB gives no recoverability benefit, only blocks drains.

Validation

kubectl kustomize builds clean for all three app dirs; renders exactly 4 PDBs (gitea 1, woodpecker 1, paperless 2) with the correct selectors.

Note: minAvailable: 1 on a single-replica workload blocks voluntary evictions entirely (drains need --disable-eviction or a scale-down first) — that friction is the intended protection for these data-bearing pods. If you would rather not block drains at all for some of these, say so and I can drop that one.

🤖 Generated with Claude Code

Fixes #201 Adds `minAvailable: 1` PodDisruptionBudgets for the data-bearing single-replica StatefulSets that had zero eviction protection: | Workload | Kind | Namespace | Selector | |---|---|---|---| | `gitea` | StatefulSet | apps | `name=gitea` | | `woodpecker-server` | StatefulSet | apps | `name=woodpecker, component=server` | | `protonmail-bridge` | StatefulSet | paperless | `name=protonmail-bridge` | | `syncthing` | StatefulSet | paperless | `name=syncthing` | On a 1-replica workload, `minAvailable: 1` makes the eviction API refuse **voluntary** evictions, so `kubectl drain` / node maintenance blocks and surfaces the pod instead of yanking a live SQLite DB or RWO-PVC pod mid-write. Involuntary disruptions (node crash, OOM) are unaffected — as intended. The woodpecker selector deliberately includes `component: server` so it does not also cover the stateless `woodpecker` agents. ### Deliberately excluded (with rationale) - **CNPG `im-pg` and `gf-pg` (instances:1)** — CloudNativePG *intentionally* creates no primary PDB for single-instance clusters (no failover target) and handles graceful primary shutdown itself (`smartShutdownTimeout`/`stopDelay`, already tuned on `im-pg`). A manual `maxUnavailable: 0` would only wedge node drains with no HA benefit and fight the operator design. `im-pg` is single-instance only during the pgvecto.rs→VectorChord migration window. - **Ceph mons** — the Rook operator self-manages quorum-preserving mon/OSD PDBs (`disruptionManagement.managePodBudgets`, default-on; no override in this repo). A hand-written PDB would be redundant or overwritten each reconcile. - **Stateless single-replica Deployments** (paperless redis/tika/gotenberg, whisper, etc.) — a blocking PDB gives no recoverability benefit, only blocks drains. ### Validation `kubectl kustomize` builds clean for all three app dirs; renders exactly 4 PDBs (gitea 1, woodpecker 1, paperless 2) with the correct selectors. Note: `minAvailable: 1` on a single-replica workload blocks voluntary evictions entirely (drains need `--disable-eviction` or a scale-down first) — that friction is the intended protection for these data-bearing pods. If you would rather not block drains at all for some of these, say so and I can drop that one. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude-bot added 1 commit 2026-07-14 10:11:28 +00:00
Adds minAvailable:1 PDBs for the data-bearing single-replica StatefulSets
that had no eviction protection: gitea, woodpecker-server, and paperless's
protonmail-bridge + syncthing sidecars. On a 1-replica workload this makes
the eviction API refuse *voluntary* evictions, so a node drain blocks and
surfaces the pod instead of yanking a live SQLite DB / RWO-PVC pod mid-write.
Involuntary disruptions (node crash, OOM) are unaffected.

Deliberately excluded, with rationale:
- CNPG clusters im-pg and gf-pg (instances:1): CloudNativePG intentionally
  omits a primary PDB for single-instance clusters (no failover target), and
  handles graceful primary shutdown itself. A manual maxUnavailable:0 would
  only wedge node drains with no HA benefit.
- Ceph mons: the Rook operator self-manages quorum-preserving mon/OSD PDBs
  (disruptionManagement.managePodBudgets default). A hand-written PDB would
  be redundant or fight the operator on each reconcile.
- Stateless single-replica Deployments (redis/tika/gotenberg caches, etc.):
  a blocking PDB gives no recoverability benefit, only blocks drains.

Refs #201

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
k.renner merged commit 2198c61d88 into master 2026-07-14 10:56:11 +00:00
Sign in to join this conversation.