Incident 2026-07-23 : un build CI sans limites a épuisé la RAM de pi1 (0 swap),
load15 >100, traefik + apiserver affamés → tout *.arcodange.lab injoignable,
Gitea compris (pourtant sain sur pi2). Personne n'est prévenu : on subit.
Quatre règles, livrées par la chaîne Telegram déjà en place (testée live) :
- NoeudInjoignable (critical) : node-exporter muet 3 min — nœud down ou noyé.
- IngressLabIndisponible (critical) : 0 replica traefik dispo, ou métrique
absente (kube-state-metrics vit sur pi1) — *.arcodange.lab est HS.
- NoeudPressionMemoire (warning) : <500 Mo dispo 5 min — le précurseur exact
de l'incident, déclenche avant le thrash.
- NoeudEnSurcharge (warning) : load15 >8 pendant 10 min.
Exprs validées contre le Prometheus live (parse + match) : pendant la rédaction,
IngressLabIndisponible et NoeudEnSurcharge matchaient l'incident en cours.
Prometheus (pi3) et Alertmanager (pi2) survivent à la perte de pi1.
Co-Authored-By: Claude Fable 5 <[email protected]>
ProspectionBriefNotSent (pushed==0, severity info) ne couvrait que la
livraison et déclenchait à tort quand l'étape brief était volontairement
sautée (metrics.py émet brief_rendered=0 aussi sur skip — seul
step_status{step="brief"} distingue skip(2) d'erreur(0)).
Remplacé par deux règles warning, gardées contre le skip :
- ProspectionBriefFailed : rendered==0 AND step_status{brief}!=2 → la
PRODUCTION de la vidéo a échoué (TTS/ffmpeg/PIL ou erreur amont) ;
- ProspectionBriefNotSent : pushed==0 AND rendered==1 → vidéo produite
mais PAS livrée sur Telegram (token/chat_id/API).
Expressions validées en live sur Prometheus (match on(job) prouvé par
requête témoin, les deux exprs vides sur l'état sain du jour). En-tête du
groupe mis à jour : la livraison AM→Telegram est câblée et testée.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Aligne le dashboard « Prospection — pipeline BI missions » sur le cahier
des charges de supervision :
- jauge 0–100 pour le meilleur score d'opportunité (au lieu d'une stat)
- nouvelle stat « Offres du brief » (prospection_brief_offres)
- bar charts « Durée par étape » et « Items par étape » sur le dernier run
(prospection_step_duration_seconds / prospection_step_items)
- cadence quotidienne : refresh 30m et plage par défaut now-7d
Provisionné via le provider Grafana « prospection » (JSON inline dans
grafana/values.yaml), datasource Prometheus (${DS_PROMETHEUS}).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
VSO refuse un VaultAuth sans vaultConnectionRef dans le ns tools
(« vaultConnectionRef must be set on resources in the "tools" namespace »),
contrairement au ns prospection qui hérite d'une connexion par défaut. On
référence la VaultConnection `default` déjà présente dans tools. Sans ça, le
Secret alertmanager-telegram n'est jamais synchronisé et le pod Alertmanager
reste bloqué sur le montage manquant.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Les règles d'alerte `prospection` s'évaluaient déjà dans Prometheus mais ne
notifiaient nulle part. On câble Alertmanager pour livrer les alertes sur le bot
Telegram de prospection, via `telegram_configs` natif d'Alertmanager.
- iac Vault : policy read-only `alertmanager-telegram` sur
kvv2/data/prospection/telegram + rôle k8s `alertmanager` (SA
prometheus-alertmanager, ns tools).
- VSO : VaultAuth + VaultStaticSecret resynchronisent kvv2/prospection/telegram
vers un Secret `alertmanager-telegram` dans le ns tools (le Secret prospection
est namespace-scoped, non réutilisable). rolloutRestartTargets sur le
StatefulSet Alertmanager.
- prometheus values : lien server -> Alertmanager (prometheus-alertmanager:9093),
route + receiver `telegram` (bot_token_file monté, chat_id inline, parse_mode
HTML, send_resolved), et montage du Secret via extraSecretMounts.
Additif : ni grafana ni les règles d'alerte existantes ne sont touchés.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Grafana tourne en SQLite sur emptyDir → migration complète du schéma à chaque
démarrage de pod, > 160 s sur Raspberry Pi. La liveson (initialDelay 60 + 10×10s)
tuait Grafana en pleine migration → CrashLoop du nouveau pod à chaque rollout
(révélé par le rollout du dashboard prospection). Ajoute une startupProbe (~10 min)
et relève failureThreshold de la liveness (filet si le chart n'expose pas startupProbe).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Consomme les métriques poussées par le pipeline prospection au Pushgateway
(job=prospection, déjà scrapé). Additif — n'affecte aucun dashboard existant.
- grafana : provider + dashboard « Prospection — pipeline BI missions » inliné
(grafana.dashboards.prospection, json) — vue d'ensemble (fraîcheur/statut/durée/
erreurs/missions/score), collecte par étape (table + historique), modèle de
données (opportunités A/B, offres/entités), livraison (brief/Telegram) + panneau
Alertes actives. Inline plutôt que ConfigMap externe : grafana est déployé via un
HelmChart CRD (tool lib), l'inline évite toute hypothèse de namespace.
- prometheus : groupe d'alertes `prospection` (serverFiles.alerting_rules.yml) —
RunStale (>25h), RunFailed, StepError, NoOffers, BriefNotSent.
NB : la livraison des alertes (Alertmanager → Telegram) n'est pas câblée dans le
cluster (server.alertmanagers vide, aucun receiver) ; les règles restent visibles
dans Prometheus /alerts + le dashboard. Câblage delivery = décision séparée.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Ajoute prospection à la liste app_policies → crée le rôle JWT gitea_cicd_prospection
et la policy prospection-ops, requis par le workflow vault.yaml du repo prospection
(qui provisionne le rôle k8s-auth + la policy de lecture kvv2/prospection/*).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds an optional kv_read_paths list to the app_policy module (default []) so an
app's env=prod runtime policy can read extra kvv2 data paths — e.g. a shared
backup-creds path owned by another app. Plumbed through the root applications
schema + module call (dynamic rule, read+list).
Set for erp: kv_read_paths = ["kvv2/data/longhorn/gcs-backup"], so the dedicated
Dolibarr backup CronJob (erp chart, gated) can read the existing GCS HMAC creds
via its own VaultStaticSecret instead of borrowing the Longhorn secret
cross-namespace or duplicating credentials.
No-op for every other app (default []). Only the `erp` runtime policy gains one
read+list rule.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The `applications` object field was declared `policies` in variables.tf, but
the cms tfvars entry, the runbook (doc/runbooks/new-web-app/03-vault-platform.md),
the guidebook (vibe/guidebooks/tools/secrets-and-vso.md) and the module input
(modules/app_policy variable `ops_policies`) all use the name `ops_policies`.
Because Terraform silently drops unknown attributes when converting a value to
an object() type, cms's `ops_policies = ["factory__cf_r2_arcodange_tf"]` was
discarded and `each.value.policies` fell back to [] — so gitea_cicd_cms never
received the `factory__cf_r2_arcodange_tf` token policy (read on
kvv1/cloudflare/r2/arcodange-tf + kvv1/zoho/self_client, defined in
factory iac/cloudflare.tf). cms CI was missing its Cloudflare R2 Terraform-state
permissions.
Fix at the root: rename the schema field `policies` -> `ops_policies` (and its
single reference main.tf:82 `each.value.policies` -> `each.value.ops_policies`),
aligning the whole chain. This is lower-churn than renaming the tfvars key (the
chosen alternative would also have required fixing the runbook + guidebook, which
both already document `ops_policies`) and prevents the next app created from the
runbook from re-introducing the same silently-dropped key.
Behavioural change: gitea_cicd_cms gains `factory__cf_r2_arcodange_tf` in its
token_policies. No other app sets this field (all default []), so no other role
changes. Reviewer: confirm the R2 policy is the intended grant for cms CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
ADR-0002 Phase D, Vault layer. `erp` gains `envs = ["prod", "sandbox"]`,
which flows into the app_policy module (main.tf:81 `envs = each.value.envs`).
For erp the module now resolves instances = ["erp", "erp-sandbox"], so the
apply:
- ADDS vault_policy.app_non_prod["erp-sandbox"] — the runtime policy
named `erp-sandbox` (read kvv2/data/erp-sandbox/* +
postgres/creds/erp-sandbox*), consumed by the sandbox pod's VSO.
- UPDATES vault_policy.ops["erp"] in place — the `erp-ops` CI policy
gains the erp-sandbox kvv2 data/delete/undelete/destroy/metadata
rules + the erp-sandbox values in the k8s-role allowed_parameter
lists, so CI can manage the sandbox instance. The glob rules
(postgres/roles/erp*, kvv1/cloudflare/erp*, auth/kubernetes/role/erp*)
already covered erp-sandbox, so they don't change.
No destroy/replace. prod `erp` runtime policy + every other app render
byte-identical (their envs still default to ["prod"]).
Diff kept to the single erp line — the pre-existing cms/crowdsec/plausible
alignment is left as-is on main (not reformatting unrelated entries).
D2 of Phase D. D1 (postgres DB+role) = factory#17 (merged). D3 (erp iac
creds + KV) and D4 (ArgoCD) follow.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Phase A of the multi-environment evolution agreed in the erp repo design
thread. Both modules gain an optional env coordinate that defaults to
"prod"; by the elision rule, env=prod produces the existing single-env
derived names character-for-character, so every existing app's tofu plan
is a no-op.
app_roles (per-instance module — caller iterates over envs):
- variables.tf: add optional env = "prod"
- main.tf: compute local.instance via elision rule + local.owner_role
(snake-case <name>_<env>_role for the Postgres owner). The name/env/
database locals are grouped so fmt keeps the existing `name` alignment
(no whitespace churn on unchanged keys).
- main.tf: substitute local.name -> local.instance / local.owner_role in
the dynamic role name, k8s role name, SA bindings, token_policies
- outputs.tf: add env + instance outputs; kvv2_path_prefix derives from
local.instance (== local.name when env=prod → backwards-compat)
app_policy (per-repo module — accepts list of envs):
- variables.tf: add optional envs = ["prod"]
- main.tf: compute local.instances + local.non_prod_instances; remove the
now-dead bound_service_account_* alias locals (the allowed_parameter
blocks build their values from per_instance_sa_* maps instead)
- main.tf: kvv2 ops rules become dynamic blocks iterating local.instances
in the original order (data, delete, undelete, destroy, metadata), so a
prod-only app renders a byte-identical policy document
- main.tf: allowed_parameter for bound_service_account_* + token_policies
use comprehensions over local.instances (1-element → identical to old
static values for prod-only apps)
- main.tf: keep vault_policy.app (env=prod runtime policy) at its original
address; add vault_policy.app_non_prod via for_each over non_prod_instances
(empty set for prod-only apps → no new resources)
Top-level wiring:
- iac/variables.tf: add envs = optional(list(string), ["prod"]) to the
applications set(object) type
- iac/main.tf: pass envs = each.value.envs to app_policies
Verified: `tofu fmt -check` clean on all touched files, `tofu validate`
passes. Backwards-compat reasoning for the no-op plan is in the PR body.
Phase B (factory postgres iac + argocd + runbook docs) and Phase D
(erp iac/main.tf for_each + activate sandbox) follow in their own PRs.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
RollingUpdate with maxSurge>0 creates a new pod before terminating the old one,
causing a Multi-Attach error on the RWO PVCs (crowdsec-db-pvc, crowdsec-config-pvc).
Recreate terminates the old pod first, then starts the new one.