Compare commits

...
Author SHA1 Message Date
claude-botandClaude Opus 5 f0f0b0af9d chore(rook-ceph): give the SAN pool the whole /29 now the nodes are off it
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The pool was .4-.6 because the nodes held .1-.3 for as long as Ceph needed
host networking to reach VLAN 49 — three addresses for three OSDs, with no
spare, during the one phase where things were actually moving.

The nodes have since given those addresses up: the link is only a macvlan
parent now and does not need one of its own. The reservation has nothing left
to avoid, so the pool takes .1 through .6.

Six for three is the headroom the migration could not afford. It leaves room
for a leaked allocation to sit until the whereabouts reconciler collects it —
the case where a node is lost hard enough that the CNI DEL never runs — and
room to add a second OSD per node without touching this file.

Keeping the pool inside the /29 rather than moving it somewhere roomier on the
segment stays deliberate. There is no router here, so an address outside the
/29 is unreachable from anything using one inside it, which is what would
break a future rolling change where migrated and un-migrated OSDs still have
to talk to each other. That was nearly the shape of this attachment and it
would have taken every pool down at the first OSD.

Inert for the running cluster: a NetworkAttachmentDefinition is read when a
pod is created, so the three existing OSDs keep .4, .5 and .6 until they next
restart for some other reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 08:03:46 +02:00
k.renner f7f04281c0 Merge pull request 'fix: [alert] AlertmanagerClusterFailedToSendAlerts (monitoring) (#649)' (#650) from alert-fix/issue-649 into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#650
2026-08-27 04:54:09 +00:00
k.renner c4a6d72cd6 Merge pull request 'fix: [alert] KubeStatefulSetReplicasMismatch (openbao) (#645)' (#648) from alert-fix/issue-645 into master
Reviewed-on: k.renner/homelab#648
2026-08-27 04:53:56 +00:00
k.renner 8c019e50d2 Merge pull request 'feat(monitoring): alert on OpenBao replicas being unavailable' (#647) from openbao-alerting into master
Reviewed-on: k.renner/homelab#647
2026-08-27 04:53:34 +00:00
security-bot c9e9830092 chore(image-automation): bump ghcr.io/music-assistant/server:2.10.0
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-08-27 01:17:36 +00:00
issue-fix-bot 7e825aff0f fix: [alert] AlertmanagerClusterFailedToSendAlerts (monitoring) (#649)
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
2026-08-26 23:45:38 +00:00
claude-botandClaude Opus 5 af81045277 feat(monitoring): alert on OpenBao replicas being unavailable
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
There was no OpenBao alerting of any kind — no rule, no ServiceMonitor. That
is why today's seal went unnoticed for two hours: the k8s-1 Talos upgrade
sealed the single replica, the ClusterSecretStore went InvalidProviderConfig,
and nothing said so.

Three replicas make this more important rather than less. Before, a seal was a
visible outage that announced itself. Now the service survives one, so a
sealed replica is a quiet loss of redundancy that nothing surfaces until the
next drain takes quorum with it.

Watches pod readiness rather than a seal metric, because there is no seal
metric to watch. OpenBao is not scraped: no ServiceMonitor exists, and its
metrics endpoint needs a token or an explicit unauthenticated_metrics_access
telemetry setting, neither configured. Verified against the running
Prometheus — no bao_* or vault_* series at all. External Secrets exports
nothing usable either, so the ClusterSecretStore going NotReady is invisible.

Readiness carries the signal for a structural reason, not by luck: a sealed
server fails its readiness probe because it can neither serve requests nor act
as a raft standby. Sealed is therefore always not-Ready. The converse does not
hold — a crash or stuck scheduling fires the same alert — but those want a
human too.

Three rules:

  OpenBaoReplicaNotReady   < 3 ready for 10m, warning. The duration only
                           ignores ordinary rescheduling; a sealed replica
                           never recovers on its own.
  OpenBaoQuorumLost        < 2 ready for 5m, critical. Raft quorum of three
                           is two, so below that the store is down.
  OpenBaoMetricsMissing    absent() for 15m, warning. Both rules above are
                           threshold comparisons, so if the series disappears
                           they stop evaluating and the silence is
                           indistinguishable from health.

Verified against the running Prometheus rather than assumed: the selector
matches (the same query with `< 4` returns the series at 3), and the alerting
thresholds correctly return nothing at 3/3 ready. Testing only that it returns
nothing would not have caught a typo in the selector.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 23:00:10 +02:00
5 changed files with 115 additions and 18 deletions
@@ -12,6 +12,7 @@ resources:
- prometheusrule-netbird-availability.yaml
- prometheusrule-netbird-backup.yaml
- prometheusrule-netbird-ingress.yaml
- prometheusrule-openbao.yaml
- prometheusrule-velero.yaml
- prometheusrule-woodpecker-rotation.yaml
# CNPG backup health. Seven Postgres clusters ship to Hetzner and nothing
@@ -0,0 +1,91 @@
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: openbao
namespace: monitoring
labels:
# kube-prometheus-stack's Prometheus resource selects on this label;
# without it the rule is created but never loaded into Prometheus.
release: kube-prometheus-stack
spec:
groups:
- name: openbao.rules
rules:
# WHY THIS WATCHES POD READINESS RATHER THAN A SEAL METRIC
#
# OpenBao is not scraped: there is no ServiceMonitor for it, and its
# metrics endpoint needs either a token or an explicit
# `unauthenticated_metrics_access` telemetry setting, neither of which
# is configured. Confirmed against Prometheus — no bao_* or vault_*
# series exist. External Secrets exports nothing usable either, so the
# ClusterSecretStore going NotReady is invisible as a metric.
#
# kube-state-metrics readiness carries the signal anyway, and does so
# for a structural reason rather than by luck: a sealed OpenBao fails
# its readiness probe, because a sealed server can neither serve
# requests nor act as a raft standby. Sealed is therefore always
# not-Ready. The converse does not hold — a crash or a stuck
# scheduling would also fire this — but both want a human too, so the
# imprecision costs nothing.
- alert: OpenBaoReplicaNotReady
# A replica that is sealed never recovers on its own, so the
# duration is only there to ignore ordinary rescheduling: a healthy
# pod moving between nodes is Ready again in well under a minute.
expr: kube_statefulset_status_replicas_ready{namespace="openbao", statefulset="openbao"} < 3
for: 10m
labels:
severity: warning
annotations:
summary: "OpenBao has {{ $value }} of 3 replicas ready"
description: >-
An OpenBao replica has been unavailable for 10 minutes. The usual
cause is a sealed replica after a node drain — every restart
seals it, and it stays out of the raft cluster until unsealed by
hand. The service is still up on the remaining replicas, so
nothing is broken yet, but redundancy is reduced and a second
drain would cost quorum. Check `kubectl -n openbao get pods` and
`bao status` in the not-ready pod; unseal with the key from
`pass` (homelab/openbao/unseal-key).
- alert: OpenBaoQuorumLost
# Raft quorum of a three-node cluster is two. At one ready replica
# the cluster cannot elect a leader and the secret store is down:
# the ClusterSecretStore goes InvalidProviderConfig and no
# ExternalSecret can refresh.
#
# Shorter window than the warning above because this is an outage,
# not a degradation — but not zero, so a rolling restart that
# briefly dips does not page.
expr: kube_statefulset_status_replicas_ready{namespace="openbao", statefulset="openbao"} < 2
for: 5m
labels:
severity: critical
annotations:
summary: "OpenBao below raft quorum — {{ $value }} of 3 replicas ready"
description: >-
Fewer than two OpenBao replicas are ready, so raft cannot hold
quorum and the secret store is unavailable. ExternalSecrets stop
refreshing; existing Secrets keep their last values, so failures
surface only when something rotates or a new secret is needed.
Unseal the sealed replicas (key in `pass`,
homelab/openbao/unseal-key) — each one individually, since a
sealed server cannot be a standby.
- alert: OpenBaoMetricsMissing
# Guards the two alerts above. Both are threshold comparisons, so if
# kube-state-metrics stops exporting this series — the StatefulSet
# deleted, the namespace gone, kube-state-metrics itself broken —
# they silently stop evaluating and the silence looks identical to
# health. This makes that state say something.
expr: absent(kube_statefulset_status_replicas_ready{namespace="openbao", statefulset="openbao"})
for: 15m
labels:
severity: warning
annotations:
summary: "No readiness metric for the OpenBao StatefulSet"
description: >-
kube-state-metrics has not reported
kube_statefulset_status_replicas_ready for the openbao
StatefulSet in 15 minutes. Either the StatefulSet is gone or
kube-state-metrics is not scraping — and while this is absent,
OpenBaoReplicaNotReady and OpenBaoQuorumLost cannot fire at all.
@@ -275,8 +275,11 @@ spec:
- alertmanager-smtp
# SMTP via Proton submission (smtp.protonmail.ch:587 STARTTLS).
# The Proton SMTP token is a per-account credential generated in
# the Proton webapp (Settings -> IMAP/SMTP) and lives in the
# alertmanager-smtp SOPS-encrypted Secret. Outbound 587 from the
# the Proton webapp (Settings -> IMAP/SMTP). It is stored in
# OpenBao at apps/monitoring/alertmanager-smtp and projected into
# the alertmanager-smtp Secret by
# ./alertmanager-smtp-externalsecret.yaml — rotating the token
# means updating OpenBao, not this repo. Outbound 587 from the
# monitoring ns is not blocked by any CNP today.
config:
global:
@@ -55,7 +55,7 @@ spec:
# stdlib only; Flux keeps the tag in lockstep via the shared image
# policy) — no extra dependency.
- name: reconcile-publish-ip
image: ghcr.io/music-assistant/server:2.9.13 # {"$imagepolicy": "flux-system:music-assistant"}
image: ghcr.io/music-assistant/server:2.10.0 # {"$imagepolicy": "flux-system:music-assistant"}
command: ["/app/venv/bin/python", "/scripts/reconcile.py"]
volumeMounts:
- name: data
@@ -65,7 +65,7 @@ spec:
readOnly: true
containers:
- name: music-assistant
image: ghcr.io/music-assistant/server:2.9.13 # {"$imagepolicy": "flux-system:music-assistant"}
image: ghcr.io/music-assistant/server:2.10.0 # {"$imagepolicy": "flux-system:music-assistant"}
env:
- name: LOG_LEVEL
value: info
@@ -29,20 +29,22 @@
# needed — unlike the v30iot attachments, there is no DHCP-supplied default
# route to confine, because there is no DHCP and no default route.
#
# The pool stays inside 10.99.0.0/29, on-link with the node addresses, and
# this is load-bearing during the migration rather than a detail. OSDs move
# one at a time, so the cluster runs mixed for a while: an OSD already on
# this attachment has to reach an OSD still using the node's host address.
# There is no router on this segment, so anything outside the /29 would be
# unreachable from the un-migrated side and the two would mark each other
# down — during precisely the window that is supposed to be safe.
# The pool is the whole usable /29, .1 through .6. It was .4-.6 while the
# migration ran, because the nodes still held .1-.3 for as long as Ceph
# needed host networking to reach this link — three addresses for three
# OSDs, no spare. The nodes have since given those up (the link is only a
# macvlan parent now and needs no address of its own), so the reservation
# has nothing left to avoid.
#
# range_start/range_end reserve .4-.6 and leave .1-.3 to the nodes. That is
# exactly one address per OSD with nothing spare, which is sufficient
# because osdMaxUpdatesInParallel bounds the migration to one OSD at a
# time: allocations grow 1, 2, 3 and never overlap. Once the switch is
# confirmed and the node addresses come out of the Talos configs, the pool
# can take the whole /29 and the spare returns.
# Keeping the pool inside the /29 rather than moving it elsewhere on the
# segment is not cosmetic. There is no router here, so an address outside
# the /29 would be unreachable from anything still using one inside it —
# which is exactly what would break a future rolling change, where migrated
# and un-migrated OSDs have to keep talking to each other.
#
# Six addresses for three OSDs is the headroom the migration could not
# afford: room for a leaked allocation to sit until the reconciler collects
# it, and room to add a second OSD per node without touching this file.
#
# IPAM
#
@@ -76,7 +78,7 @@ spec:
"ipam": {
"type": "whereabouts",
"range": "10.99.0.0/29",
"range_start": "10.99.0.4",
"range_start": "10.99.0.1",
"range_end": "10.99.0.6"
}
}