Files
home-network-k3s-impl/platform/monitoring
jakman85andClaude Opus 5 a3f4ddb971 merge: feat/egress-gateway-staleness-alert — finished so it can actually pass the gate
Brings the two alerts for the silent egress-gateway failure: VPNEgressSteeringStale
(critical) and VPNEgressUplinkLabelMissing (warning), plus the kube-state-metrics
allowlist entry that makes them non-inert.

WHY THE BRANCH SAT IN THE BACKLOG: it could not pass the merge gate. Three
whole-repo checks failed on it, and each one was a real gap rather than a nuisance:

  * alert-runbooks-resolvable — both alerts link to /runbooks/vpn-egress-stale,
    which did not exist. An alert firing now would hand the operator a 404.
  * alert-rules-unit-tested — alerts.yaml is already a covered file, so new rules
    in it must be tested or a "tested" file quietly grows untested rules.
  * alert-metrics-have-scrape-targets — see the guard bug below.

WHAT THE ALERTS CATCH. egress-class=vpn pods leave through one gateway node's tun0;
Cilium resolves it to an ifindex and caches it. An OpenVPN reconnect destroys and
re-creates tun0 with a NEW ifindex, and the node label
homelab.keithjackson.dev/egress-uplink=<addr>-<ifindex> is what forces Cilium to
re-resolve. If the label does not follow the tunnel, the cache points at a destroyed
interface and every vpn-class pod is silently dropped — while tun0 stays up and
VPNTunnelDown says nothing. That ran ~80 minutes undetected on the dev gateway.

Verified this is live on prod, not inert: k3s-prod-3 carries egress-gateway=true and
egress-uplink=10.150.192.86-139, and vpn-egress-steering has been in place 14 days.
The label was exported on 0 series before this change, so the allowlist entry is
what switches the alerts on.

runbooks/vpn-egress-stale.md — written and wired into the configMapGenerator, with
the four required operator sections. It leads with proving egress from a pod the
policy selects, because the symptom is "outbound traffic silently disappears", and
it says plainly not to "fix" this by deleting the policy: that does not restore
egress, it sends vpn-class traffic out of the real ISP connection, which is the leak
the class exists to prevent.

platform/monitoring/tests/vpn-egress.test.yaml — four promtool fixtures. 1 and 2 are
the SAME tunnel reconnect with and without the label following it, which isolates the
mechanism the rule actually keys on: a label VALUE change is not a changing sample,
it retires one series and starts another. 3 is the inert case (gateway node, no uplink
label) where the critical rule CANNOT fire, which is why the warning exists.

Two fixes the branch needed that were defects in the checks themselves:

1. alert-metrics-have-scrape-targets asserted the KSM allowlist matched
   `nodes=[...egress-gateway]` with a literal closing bracket — i.e. that the list
   contained EXACTLY that one label. Adding a second label correctly (egress-uplink,
   which these alerts need) therefore FAILED the check while the property it guards
   was perfectly satisfied. Loosened to `[,\]]`: in the list, not the whole list. A
   guard that breaks when you correctly extend the thing it guards teaches people to
   edit the guard, which is how it stops guarding.

2. UNTESTED_ALERTS listed VPNTunnelDown, and the check flagged it as stale because
   "asserted on" is a bare word match over the test file — my header COMMENT
   mentioning VPNTunnelDown was enough to count as coverage. Rather than delete the
   register entry and claim coverage that did not exist, the fixtures now assert
   VPNTunnelDown QUIET in all four cases. That required making them faithful: they
   had no node_network_receive_bytes_total{device="tun0"} at all, so VPNTunnelDown
   would have been firing throughout unnoticed, because promtool only checks the
   alertnames you name. A healthy tun0 passes traffic, and that silence IS the claim
   these rules exist for.

NOTE FOR LATER, not fixed here: that word-match means any alert named in prose counts
as tested. Tightening it to `alertname:` positions could reveal other prose-only
"coverage", and the register is shrink-only, so the cascade wants doing deliberately
rather than at 01:00 unattended.

Conflicts resolved: kept HEAD's Velero custom-resource-state block and took only the
branch's one-line allowlist change (14 lines down to 1); inserted the branch's
2026-09-12 DECISIONS-LOG entry in date order rather than appending it out of sequence.

Gated: offline l0,l1 --include-scope whole-repo — 24 cases, 0 failures, 0 skips,
0 unwired. The new unit tests pass under the pinned promtool 3.14.0.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-19 00:52:06 -05:00
..
2026-08-24 19:52:25 -05:00

platform/monitoring

Observability stack — kube-prometheus-stack (Prometheus + Alertmanager + Grafana) + Loki + Grafana Alloy + platform-health PrometheusRules.

Contents

File What
namespace.yaml monitoring namespace
helmrepositories.yaml prometheus-community + grafana Flux HelmRepositorys
kube-prometheus-stack.yaml HelmRelease — Prometheus, Alertmanager (→ SMTP email), Grafana, kube-state-metrics, node-exporter
loki.yaml HelmRelease — Loki single-binary, filesystem, compactor-enforced retention
alloy.yaml HelmRelease — Grafana Alloy DaemonSet, tails pod logs → Loki
alerts.yaml Platform-health PrometheusRules (R-OBS-04)
tests/ promtool test rules unit tests over the committed PrometheusRules — each rule pinned quiet on the good state and firing on the bad one (tests/run.sh — the repo-wide harness, it also runs other components' companion *.promtool-test.yaml; enforced by the L0 check alert-rules-unit-tested)
kustomization.yaml builds all of the above

Requirement mapping

  • R-OBS-01 (what is monitored): Prometheus + node-exporter + kube-state-metrics for metrics; Alloy → Loki for logs. serviceMonitorSelectorNilUsesHelmValues: false so ServiceMonitors from every namespace (and those rendered from ServiceDefinitions, docs/13) are scraped by default.
  • R-OBS-02 (alert delivery): Alertmanager routes by severity (critical vs warning) to email. SMTP creds are a SOPS secret referenced by name only (see below).
  • R-OBS-03 (dashboards): Grafana, with the dashboard/datasource sidecar watching grafana_dashboard-labeled ConfigMaps across all namespaces (per-service dashboards land here). Grafana is part of the C2 UI composition (docs/12), later behind Keycloak SSO (ADR-0014).
  • R-OBS-04 (platform health): alerts.yaml — first-class alerts for nodes, Longhorn, Flux, cert-manager, networking/VPN/Hubble, Velero backups, Keycloak/IdP, and the monitor's own capacity. Split critical/warning per the docs/10 severity model.

SMTP secret (operator hand-off)

Alertmanager reads the SMTP password from a mounted SOPS-encrypted Secret — referenced by name, never inlined. The operator must provide (out of band, SOPS-encrypted per docs/07):

kind: Secret
metadata:
  name: alertmanager-smtp        # <-- referenced in kube-prometheus-stack.yaml (alertmanagerSpec.secrets)
  namespace: monitoring
stringData:
  smtp-password: "<app password>"   # mounted at /etc/alertmanager/secrets/alertmanager-smtp/smtp-password

The non-secret SMTP fields (smtp_smarthost, smtp_from, smtp_auth_username, and the receiver to: address) are REPLACE-ME placeholders in kube-prometheus-stack.yaml for the operator to fill (same convention as velero/). Grafana's admin bootstrap likewise references a SOPS secret by name — grafana-admin-credentials (keys admin-user / admin-password).

Dev trims (the ~22 GiB RAM ceiling)

This subtree is authored to stay light on the single dev workstation:

  • Loki chunksCache/resultsCache DISABLED — the grafana/loki chart's default memcached caches request ~8-9 GiB each; leaving them on would blow the budget by itself. Also disabled: the nginx gateway, lokiCanary, and test hooks. Loki runs SingleBinary (one pod).
  • Single replicas everywhere (Prometheus, Alertmanager, Grafana, Loki).
  • Modest requests/limits on every component (see values). Ephemeral (emptyDir) state for Alertmanager and Grafana in dev.
  • k3s control-plane scrape jobs disabled (kubeControllerManager/kubeScheduler/kubeEtcd/ kubeProxy) — embedded in k3s, not separately scrapable; keeps targets green and RAM down.

Approximate steady-state memory requests (what the scheduler reserves):

Component Req Notes
Prometheus 384Mi limit 1Gi
Alertmanager 32Mi
Grafana 128Mi
prometheus-operator 96Mi
kube-state-metrics 64Mi
node-exporter ×3 96Mi 32Mi/node DaemonSet
Loki (single binary) 192Mi limit 512Mi
Alloy ×3 288Mi 96Mi/node DaemonSet
Total requests ~1.3 GiB working set typically ~1.5-2.5 GiB; limit ceiling ~3 GiB

Comfortably within the 22 GiB box alongside the rest of the platform.

Environment knobs (R-ENV-02)

Only three substitution vars touch this subtree; the dev overlay (clusters/dev) patches cluster-vars to dev values:

Var base dev
${MONITORING_PROMETHEUS_RETENTION} 7d 3d
${MONITORING_PROMETHEUS_PVC_SIZE} 10Gi 8Gi
${MONITORING_PROMETHEUS_RETENTION_SIZE} 8GiB 5GiB
${MONITORING_LOKI_PVC_SIZE} 10Gi 5Gi

MONITORING_PROMETHEUS_RETENTION_SIZE is a size-based retention cap (Prometheus *iB units, distinct from the K8s Gi PVC knob) and must stay below MONITORING_PROMETHEUS_PVC_SIZE. Time-only retention prunes nothing until the age window elapses, so a young/busy cluster fills /prometheus first; a full disk wedges every WAL commit (scrapes still 200 but samples are dropped, the head freezes) and makes every absent()/==0 platform alert fire falsely. The size cap drops the oldest blocks before the disk fills. (prod overlay: PVC 50Gi, cap 45GiB.)

PVCs use the replicated longhorn StorageClass (ADR-0003).

Notes / known gaps for bring-up

  • Loki log retention has no dedicated knob. Only the three vars above exist. Loki's retention_period is hardcoded to 72h (compactor-enforced) to mirror dev metrics retention. Promote to a cluster-vars knob (in clusters/, out of this subtree) if per-env log retention is wanted.
  • Several R-OBS-04 rules depend on exporters/ServiceMonitors owned by other workstreams (Flux, cert-manager, Longhorn, Velero, Keycloak, Cilium/Hubble). Rules are authored to fire once those series arrive; metric names marked TODO(WS-*) in alerts.yaml (notably the egress VPN metric) need confirming against the deployed exporter.
  • Hubble network-flow visibility itself is delivered by platform/cilium (docs/03); this subtree only consumes its metrics for denied-flow alerts.

Validate (no deploy)

kubectl kustomize platform/monitoring

Author-only — nothing here is applied to a cluster; Flux reconciles it at deploy time.