The operator got no mail when Flux began applying a git change and none when it finished. Measured cause: notification-controller v1.4.0 is running and its CRDs serve v1beta3, but there are ZERO Provider and ZERO Alert objects in the cluster — nothing in git ever created any. The obvious fix does not work. Neither Flux event can be filtered into a deploy signal: ReconciliationSucceeded is a heartbeat (101 events in one hour across 16 Kustomizations, message byte-identical whether the apply changed anything), and Progressing is worse (five Kustomizations re-apply the same object on every tick — event counts 2505/2504/2501/2495/2490). gotk_* carries no revision label at all (gotk_reconcile_condition 120 series, gotk_suspend_status 40, none with one). The applied revision is the only signal that moves when and only when git moves, and it lives in object status alone. So export it via the kube-state-metrics custom-resource-state path that already serves the Velero Schedule metrics — kube_flux_kustomization_info/_suspended and kube_flux_gitrepository_info — and ask the question in PromQL: FluxRolloutInProgress (severity: info, no `for:`) fires the moment a non-suspended Kustomization's applied revision is not the revision the GitRepository fetched from main, and CLEARS when all have caught up. Fired mail = a deploy started; RESOLVED mail = it completed. The completion half is send_resolved, not a second rule. FluxRolloutStuck (severity: warning, for: 30m) is the same expression as the escalation for a rollout that started and never converged. Two guards, each with a test whose only job is to fail if the clause is removed: `and on () (count(gitrepository_info) > 0)` — without it a missing exporter makes `unless on (revision) <empty>` remove nothing and all 16 Kustomizations alert forever, the permanently-firing mode this repo shipped once already — and the suspended-Kustomization exclusion, without which a suspended tier holds the rollout open and the "applied" mail never arrives. A dedicated Alertmanager severity=info route + email-deploy receiver keeps deploy notices off the [WARNING] catch-all. podAnnotations carries a roll-counter because the chart sets no checksum over the custom-resource-state ConfigMap, so a config-only edit would leave KSM on the old collector set with the new alerts silently inert. Verified: promtool SUCCESS on 6 scenarios (both guards, plus the convergence test that proves the alert empties itself); lint/l0/render-golden/l1/decisions-log/ spec-immutability green in the pinned toolbox. NOT verified: no mail has been sent — the metrics do not exist until this merges. Full gap list, and the deliberately-cut notification-controller half, in the handoff entry. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
119 lines
7.0 KiB
Markdown
119 lines
7.0 KiB
Markdown
# flux-rollout — a git change is rolling out, or has stopped halfway
|
|
|
|
**Alerts:** `FluxRolloutInProgress` (info, no `for:`) and `FluxRolloutStuck` (warning, 30m)
|
|
**File:** `platform/monitoring/alerts.yaml`, group `platform-health.flux`
|
|
**Metrics:** `kube_flux_kustomization_info` / `kube_flux_kustomization_suspended` /
|
|
`kube_flux_gitrepository_info` — kube-state-metrics custom-resource-state, configured in
|
|
`platform/monitoring/kube-prometheus-stack.yaml`
|
|
|
|
## What this means here
|
|
|
|
These two are the **deploy notification**, not a fault report. On this cluster merging a PR into
|
|
`main` IS the deploy, and until this pair existed nothing told you a deploy had begun: the Flux event
|
|
stream emits `ReconciliationSucceeded` on every 10-minute tick whether or not anything changed (101
|
|
such events in one hour, measured 2026-09-29), so "applying a commit" and "idle" looked identical.
|
|
|
|
The pair reads the one signal that moves only when git moves — the **applied revision**:
|
|
|
|
- `GitRepository/flux-system` `.status.artifact.revision` — what `main` is (polled every 1m)
|
|
- `Kustomization/*` `.status.lastAppliedRevision` — what each tier has actually applied
|
|
|
|
**`FluxRolloutInProgress` fires** the moment any non-suspended Kustomization's applied revision is
|
|
not the revision fetched from `main`, and **clears when every one of them has caught up**. So:
|
|
|
|
| Mail | Subject | Means |
|
|
|---|---|---|
|
|
| firing | `[FLUX] applying — FluxRolloutInProgress` | a commit started rolling out |
|
|
| resolved | `[FLUX] applied — FluxRolloutInProgress` | every Kustomization reached it; rollout complete |
|
|
|
|
Both are expected, routine, and not a problem. The `severity: info` label routes them to the
|
|
dedicated `email-deploy` Alertmanager receiver so they never arrive subject-lined `[WARNING]`.
|
|
|
|
**`FluxRolloutStuck` is the one that matters.** Same expression, `for: 30m`, `severity: warning`. It
|
|
means a deploy started and did not converge — the "applying" mail arrived and the "applied" mail
|
|
never did. The 14-tier chain is dependency-ordered and revision-gated, so a single stalled tier holds
|
|
every tier below it on "dependency not ready".
|
|
|
|
## First commands
|
|
|
|
`kubectl` does not work on the workstation — the toolchain lives only in the pinned container, and
|
|
the context must always be named explicitly.
|
|
|
|
Which tiers are behind, and on what:
|
|
|
|
```bash
|
|
./infra/devcontainer/dev.sh run bash -c '
|
|
kubectl --context k3s-prod -n flux-system get gitrepository flux-system \
|
|
-o jsonpath="WANTED: {.status.artifact.revision}{\"\n\"}"
|
|
kubectl --context k3s-prod get kustomizations -A \
|
|
-o custom-columns="NAME:.metadata.name,APPLIED:.status.lastAppliedRevision,READY:.status.conditions[?(@.type==\"Ready\")].status"'
|
|
```
|
|
|
|
Any row whose `APPLIED` differs from `WANTED` is a tier this alert is naming. Then ask **why** that
|
|
tier is not moving — its Ready condition carries the reason:
|
|
|
|
```bash
|
|
./infra/devcontainer/dev.sh run bash -c '
|
|
kubectl --context k3s-prod -n flux-system get kustomization <name> \
|
|
-o jsonpath="{range .status.conditions[*]}{.type}={.status} {.reason}: {.message}{\"\n\"}{end}"'
|
|
```
|
|
|
|
Confirm the alert is reading live data rather than a stale exporter:
|
|
|
|
```bash
|
|
./infra/devcontainer/dev.sh run bash -c '
|
|
POD=$(kubectl --context k3s-prod -n monitoring get pod -l app.kubernetes.io/name=prometheus -o name | head -1)
|
|
kubectl --context k3s-prod -n monitoring exec "$POD" -c prometheus -- \
|
|
/bin/promtool query instant http://localhost:9090 "count by (__name__) ({__name__=~\"kube_flux.*\"})"'
|
|
```
|
|
|
|
Three names must come back (`kube_flux_kustomization_info`, `kube_flux_kustomization_suspended`,
|
|
`kube_flux_gitrepository_info`). If that query is **empty**, the alerts are inert — go to the last
|
|
cause below.
|
|
|
|
## Likely causes, most likely first
|
|
|
|
1. **Nothing is wrong — the rollout is simply in flight.** Most Kustomizations reconcile on a 10m
|
|
interval and the chain is sequential, so several minutes between the "applying" and "applied"
|
|
mails is normal. `cilium` is on a 1h interval and can legitimately be the last to converge.
|
|
2. **A gating tier is unhealthy, so everything below it is parked.** Look for `DependencyNotReady`
|
|
in the stuck tier's message and walk *up* the chain to the first tier that is not Ready; that one
|
|
is the real fault. Its own alert (`FluxReconciliationFailure`) and runbook `flux-reconcile` own it.
|
|
3. **A Kustomization is suspended.** A suspended tier is excluded from this alert by design (so it
|
|
cannot hold the "applied" mail open forever), but anything that `dependsOn` it still stalls and
|
|
*will* show here. `FluxResourceSuspended` / runbook `flux-suspended` covers it.
|
|
4. **A health check is timing out.** A `wait: true` tier only records `lastAppliedRevision` once its
|
|
objects pass health checks, so a workload that never goes Ready keeps the tier on the old
|
|
revision even though the manifests applied fine.
|
|
5. **kube-state-metrics is not publishing the `kube_flux_*` family.** Then the rules are inert and
|
|
you would get *no* mail at all rather than a stuck one — so this is the cause when the complaint
|
|
is silence. The metrics come from `customResourceState` plus a read-only RBAC rule for
|
|
`kustomize.toolkit.fluxcd.io` / `source.toolkit.fluxcd.io`; without that RBAC, KSM emits the set
|
|
**empty** and logs a forbidden error. Check its logs:
|
|
```bash
|
|
./infra/devcontainer/dev.sh run bash -c '
|
|
kubectl --context k3s-prod -n monitoring logs deploy/kube-prometheus-stack-kube-state-metrics \
|
|
--tail=50 | grep -i "forbidden\|custom.resource" || echo "no RBAC complaints"'
|
|
```
|
|
Note the chart puts **no checksum annotation** over that ConfigMap, so a change to the config
|
|
alone does not restart the pod — that is why `podAnnotations` carries a
|
|
`homelab.keithjackson.dev/crs-config-rollout` counter that must be bumped in the same commit.
|
|
|
|
## When to escalate or stop
|
|
|
|
- **`FluxRolloutInProgress` alone, cleared within ~15 minutes: do nothing.** That is the feature
|
|
working. Do not suspend, reconcile or apply anything in response to it.
|
|
- **`FluxRolloutStuck` firing: fix the upstream fault, not this symptom.** This alert names the
|
|
tier that is behind; it never names the cause. Walk up to the first non-Ready tier and use
|
|
`flux-reconcile`.
|
|
- **Never `kubectl apply`, `edit` or `patch` the change through by hand.** Flux reconciles `main`;
|
|
a hand-applied object is drift that the next reconcile reverts or, worse, adopts — and it makes
|
|
the applied-revision signal these alerts read a lie. `flux reconcile` (an operator action, never
|
|
an agent's) is the only nudge that is safe, and it only *hurries* the same pull.
|
|
- **Escalate to a revert if a stuck rollout is degrading service.** Reverting the offending commit
|
|
on `main` through the normal PR flow is the rollback path; see runbook `update-rollback`.
|
|
- **If both alerts are silent during a deploy you know happened,** the notification itself is broken,
|
|
not the deploy. That is cause 5 above, and it is worth fixing immediately — silence from these
|
|
rules is indistinguishable from "no deploys happened", which is the exact defect they were
|
|
written to close.
|