Author SHA1 Message Date
arcodangeandClaude Opus 5 d146affbcd fix(minio) — déclarer minio dans la liste centrale des applications Vault
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Helm Charts / Detect changed charts (push) Successful in 1m6s
Helm Charts / Detect changed charts (pull_request) Successful in 24s
Hashicorp Vault / Auth with gitea for vault (push) Failing after 8m38s
Hashicorp Vault / Tofu - Vault IAC (push) Has been skipped
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Library charts tool (push) Has been skipped
Hashicorp Vault / Tofu - Vault IAC (pull_request) Has been skipped
Hashicorp Vault / Auth with gitea for vault (pull_request) Failing after 8m40s
Helm Charts / Application charts pgcat (push) Has been skipped
CI en échec sur le workflow MinIO : role "gitea_cicd_minio" could not be found.

Diagnostic : les rôles CI gitea_cicd_<app> ne naissent PAS dans l'IaC de
l'application — ils viennent du module app_policy, appliqué centralement par
hashicorp-vault/iac pour chaque entrée de terraform.tfvars. J'avais posé
minio/iac (qui s'authentifie AVEC ce rôle) sans alimenter la liste : le run
tentait donc de s'authentifier avec un rôle que personne n'avait créé. Amorçage
circulaire, entièrement de mon fait.

- hashicorp-vault/iac/terraform.tfvars : minio ajouté aux applications, avec
  service_account_namespaces = ["tools"] comme crowdsec et plausible ;
- .gitea/workflows/vault.yaml : les triggers couvrent désormais *.tfvars en
  plus de *.tf. C'est là que vit la liste des applications : sans ça, ajouter
  une app ne déclenchait jamais la création de son rôle — le piège qui vient
  de mordre. Les ancres YAML des triggers sont retirées au passage (une ancre
  dans un trigger Gitea Actions fait taire push ET pull_request en silence,
  vécu sur arcodange/kadans, issues 113→117 ; c'est probablement pourquoi ce
  workflow ne partait qu'à la main) ;
- minio/README.md : l'ordre de mise en service dit maintenant les DEUX étapes
  et nomme l'erreur exacte à laquelle on s'expose en les inversant.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CoafGWmRVESaWX819USUUA
2026-07-25 18:23:02 +02:00
arcodange 2202e7bbfe Merge pull request 'feat(minio) — stockage objet S3 du homelab (brique partagée de tools)' (#18) from claude/minio into main
MinIO / Auth with gitea for vault (push) Successful in 43s
MinIO / Tofu - minio IAC (push) Failing after 13s
Helm Charts / Detect changed charts (push) Successful in 1m3s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Reviewed-on: #18
2026-07-25 18:17:07 +02:00
arcodangeandClaude Opus 5 84edfc8640 feat(minio) — stockage objet S3 du homelab (brique partagée de tools)
Helm Charts / Detect changed charts (push) Successful in 16s
Helm Charts / Detect changed charts (pull_request) Successful in 15s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Library charts tool (pull_request) Has been skipped
MinIO / Tofu - minio IAC (pull_request) Has been skipped
MinIO / Auth with gitea for vault (pull_request) Failing after 8m32s
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Le fondateur : « on déploie MinIO dans le repo tools du homelab non ? » — oui,
c'est bien le pattern : le dossier tools/ porte les briques PARTAGÉES
(pgbouncer, clickhouse, grafana…) et le namespace tools les fait tourner ; les
charts applicatifs vivent dans le repo de leur app.

Décidé de longue date côté produit, jamais déployé : ADR-012 « MinIO local
d'abord » (bascule R2 à 100+ utilisateurs / 10 To par mois), ADR-013 (le gratuit
reste local-first, MinIO sert les paliers payants). Vérifié avant d'écrire :
aucun pod ni service MinIO dans le cluster.

Le chart suit la recette du repo (dépendance à la library "tool" + chart amont
en SubChart, deux gardes dans templates/) :

- mode STANDALONE, 1 réplique : ce qui transite est DÉRIVÉ (le master d'une
  vidéo reste sur l'appareil de son propriétaire, ADR-018) et Longhorn réplique
  déjà le volume — l'erasure coding distribué coûterait de la RAM que des Pi 5
  n'ont pas à dépenser pour ça ;
- 50 Gi sur longhorn ≈ 250 h de cours au palier « travail » (360p, 3,4 Mo/min) ;
  ⚠ Longhorn réplique : compter ×3 sur la capacité avant d'augmenter ;
- ressources bornées (512 Mi / 2 Gi) : la limite protège les voisins de tools ;
- API s3.arcodange.lab + console minio.arcodange.lab (Traefik) ;
- bucket kadans-videos PRIVÉ — l'accès passera par des URL signées (ADR-0002) ;
- identifiants JAMAIS au dépôt : iac/ les génère dans Vault (kvv2/minio/config),
  le Vault Secrets Operator les matérialise, le chart les lit via existingSecret.
  Le SA du pod est nommé "minio" (pas le "minio-sa" amont) car le module
  app_roles borne l'authentification au SA portant le nom de l'app.

⚠ Le workflow minio.yaml écrit ses triggers EN TOUTES LETTRES : une ancre YAML
dans un trigger Gitea Actions fait taire push ET pull_request en silence (vécu
sur arcodange/kadans, issues 113→117). Les workflows plausible/crowdsec/vault de
ce repo en utilisent encore — à vérifier séparément, c'est probablement
pourquoi ils ne partent qu'à la main.

Vérifié : helm dependency update + helm template (11 ressources rendues, SA et
VaultAuth cohérents) + helm lint ✓.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CoafGWmRVESaWX819USUUA
2026-07-25 16:49:34 +02:00
arcodange 342c4ab346 Merge pull request 'feat(grafana): exposer grafana.arcodange.fr via le tunnel Cloudflare (pattern kadans)' (#16) from arcodange/grafana-fr into main
Helm Charts / Detect changed charts (push) Successful in 58s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Reviewed-on: #16
2026-07-24 21:59:13 +02:00
arcodangeandClaude Fable 5 5fbc7f6ff8 feat(grafana): exposition publique grafana.arcodange.fr via le tunnel (pattern kadans)
Helm Charts / Detect changed charts (push) Successful in 17s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Helm Charts / Detect changed charts (pull_request) Successful in 23s
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Second Ingress (grafana-public) en PLUS du .lab, comme kadans :
- entrypoint `web` — TLS terminé en amont par le tunnel Cloudflare
  (wildcard *.arcodange.fr → traefik:80, rien à changer côté cms/cloudflare) ;
- middleware crowdsec (convention .fr) ;
- pas de basic-auth : Grafana a sa propre authentification (aucun accès
  anonyme dans grafana.ini), contrairement à kadans qui est ouvert.

Le .lab garde son localIp@file ; rendu par le wrapper SubChart et appliqué
par ArgoCD (précédent : prometheus/templates/vault-telegram.yaml).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-24 21:28:16 +02:00
arcodange bbf6653464 Merge pull request 'feat(observabilité) — jobs d'analyse Kadans : scrape du worker laptop + dashboard Grafana' (#15) from arcodange/observabilite-kadans into main
Helm Charts / Detect changed charts (push) Successful in 20s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
2026-07-24 21:24:35 +02:00
arcodangeandClaude Fable 5 347ebee1b0 feat(observabilité) — jobs d'analyse Kadans : scrape du worker LAPTOP + dashboard Grafana
Helm Charts / Detect changed charts (push) Successful in 1m10s
Helm Charts / Detect changed charts (pull_request) Successful in 1m1s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
- prometheus : cible statique kadans-worker-mac (192.168.1.103:9105, tier
  laptop). Un scrape raté n'est PAS un incident : le Mac dort, up==0 raconte
  la latence — ne pas alerter dessus. ⚠ réserver l'IP au routeur (DHCP).
- grafana : provider + dashboard « Kadans — jobs d'analyse » (uid
  kadans-jobs-analyse) : worker en écoute / dernier poll / file pending + âge
  du plus ancien (seuils 1 h / 24 h = le SLO), file par statut et publications
  par issue (couleurs de STATUT sémantiques), lanes exécutées et durée moyenne
  par lane. La façade, elle, arrive par les annotations de son pod
  (kadans-jobs#3) — zéro config ici.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-24 21:17:06 +02:00
arcodange 53710d9fde Merge pull request 'feat(alerting): 5e règle homelab — CertificatNonRenouvele, le mode de panne du 24/07 au matin' (#14) from arcodange/homelab-alerts-cert into main
Helm Charts / Detect changed charts (push) Successful in 1m3s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Reviewed-on: #14
2026-07-24 12:51:52 +02:00
arcodangeandClaude Fable 5 6f398f4ddc feat(alerting): 5e règle homelab — CertificatNonRenouvele (<4 h restantes)
Helm Charts / Detect changed charts (pull_request) Successful in 22s
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Helm Charts / Detect changed charts (push) Successful in 1m8s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Les 4 premières règles n'auraient rien vu le matin du 2026-07-24 : nœuds up,
traefik up… mais wildcard 24 h expiré (renouvellement en échec silencieux
depuis des heures). certmanager_certificate_expiration_timestamp_seconds est
déjà scrapée (vérifié live : 2 séries). Cert-manager renouvelle à ~16 h d'âge ;
<4 h restantes = plusieurs cycles ratés → critical, 15 min de grâce.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-24 10:17:18 +02:00
arcodange ffce6990a3 Merge pull request 'feat(alerting): groupe homelab — être prévenu sur Telegram quand (ou juste avant que) le lab tombe' (#13) from arcodange/homelab-alerts into main
Helm Charts / Detect changed charts (push) Successful in 1m9s
Helm Charts / Application charts pgcat (push) Has been skipped
Helm Charts / Library charts tool (push) Has been skipped
Reviewed-on: #13
2026-07-24 00:29:29 +02:00
arcodangeandClaude Fable 5 b9626d878b feat(alerting): groupe homelab — alerté quand (ou juste avant que) le lab tombe
Helm Charts / Detect changed charts (pull_request) Successful in 2m2s
Helm Charts / Detect changed charts (push) Successful in 10m9s
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
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]>
2026-07-24 00:12:37 +02:00
arcodange bc62304bc3 Merge pull request 'fix(alerting): alerte fiable sur l'échec de la vidéo quotidienne du brief' (#12) from arcodange/alert-brief-video into main
Helm Charts / Detect changed charts (push) Successful in 21s
Helm Charts / Application charts pgcat (push) Has been skipped
Helm Charts / Library charts tool (push) Has been skipped
2026-07-23 22:06:15 +02:00
arcodangeandClaude Opus 4.8 8e236230d3 fix(alerting): alerte fiable sur l'échec de la vidéo quotidienne du brief
Helm Charts / Detect changed charts (push) Successful in 2m50s
Helm Charts / Detect changed charts (pull_request) Successful in 18s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
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]>
2026-07-23 22:05:45 +02:00
17 changed files with 619 additions and 12 deletions
+70
View File
@@ -0,0 +1,70 @@
---
name: MinIO
# ⚠ Triggers écrits EN TOUTES LETTRES, sans ancre YAML (`&`/`*`).
# Une ancre dans un trigger Gitea Actions fait taire push ET pull_request —
# en silence, aucun run, aucune erreur (vécu sur arcodange/kadans, issues 113
# → 117). Les autres workflows de ce repo utilisent encore des ancres : à
# vérifier séparément, c'est probablement pour ça qu'ils ne partent qu'à la
# main (workflow_dispatch).
on:
workflow_dispatch: {}
push:
paths:
- 'minio/**/*.tf'
pull_request:
paths:
- 'minio/**/*.tf'
# cancel any previously-started, yet still active runs of this workflow on the same branch
concurrency:
group: ${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true
.vault_step: &vault_step
name: read vault secret
uses: https://gitea.arcodange.lab/arcodange-org/vault-action.git@main
id: vault-secrets
with:
url: https://vault.arcodange.lab
caCertificate: ${{ secrets.HOMELAB_CA_CERT }}
jwtGiteaOIDC: ${{ needs.gitea_vault_auth.outputs.gitea_vault_jwt }}
role: gitea_cicd_minio
method: jwt
path: gitea_jwt
secrets: |
kvv1/google/credentials credentials | GOOGLE_BACKEND_CREDENTIALS ;
kvv1/gitea/tofu_module_reader ssh_private_key | TERRAFORM_SSH_KEY ;
jobs:
gitea_vault_auth:
name: Auth with gitea for vault
runs-on: ubuntu-latest
outputs:
gitea_vault_jwt: ${{steps.gitea_vault_jwt.outputs.id_token}}
steps:
- name: Auth with gitea for vault
id: gitea_vault_jwt
run: |
echo -n "${{ secrets.vault_oauth__sh_b64 }}" | base64 -d | bash
tofu:
name: Tofu - minio IAC
needs:
- gitea_vault_auth
runs-on: ubuntu-latest
env:
OPENTOFU_VERSION: 1.8.2
TERRAFORM_VAULT_AUTH_JWT: ${{ needs.gitea_vault_auth.outputs.gitea_vault_jwt }}
VAULT_CACERT: "${{ github.workspace }}/homelab.pem"
steps:
- *vault_step
- uses: actions/checkout@v4
- name: prepare vault self signed cert
run: echo -n "${{ secrets.HOMELAB_CA_CERT }}" | base64 -d > $VAULT_CACERT
- name: terraform apply
uses: dflook/terraform-apply@v1
with:
path: minio/iac
auto_approve: true
+14 -3
View File
@@ -2,12 +2,23 @@
# template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml
name: Hashicorp Vault
on: #[push,pull_request]
# ⚠ Triggers écrits EN TOUTES LETTRES, sans ancre YAML (`&`/`*`) : une ancre
# dans un trigger Gitea Actions fait taire push ET pull_request, en silence
# (vécu sur arcodange/kadans, issues 113 → 117) — c'est probablement pourquoi
# ce workflow ne partait qu'à la main.
# Et `*.tfvars` compte AUTANT que `*.tf` : la liste des applications (donc les
# rôles gitea_cicd_<app>) vit dans terraform.tfvars — l'oublier, c'est ajouter
# une app sans jamais créer son rôle.
on:
workflow_dispatch: {}
push: &vaultPaths
push:
paths:
- 'hashicorp-vault/**/*.tf'
pull_request: *vaultPaths
- 'hashicorp-vault/**/*.tfvars'
pull_request:
paths:
- 'hashicorp-vault/**/*.tf'
- 'hashicorp-vault/**/*.tfvars'
# cancel any previously-started, yet still active runs of this workflow on the same branch
concurrency:
+1
View File
@@ -9,3 +9,4 @@ tools:
grafana: {}
plausible: {}
prometheus: {}
minio: {}
+33
View File
@@ -0,0 +1,33 @@
# Exposition PUBLIQUE grafana.arcodange.fr, EN PLUS du .lab interne (qui reste
# inchangé derrière son middleware localIp@file). Pattern kadans
# (chart/templates/ingress-public.yaml) : entrypoint `web` — le TLS est terminé
# en amont par le tunnel Cloudflare (wildcard *.arcodange.fr →
# traefik.kube-system.svc:80, cf. cms/cloudflare module cf_tunnel) — et
# middleware crowdsec (WAF/bouncer, convention .fr).
# Pas de basic-auth ici, contrairement à kadans : Grafana a sa propre
# authentification (login requis, pas d'accès anonyme dans grafana.ini).
#
# NB : ce chart grafana est en mode `tool.kind: SubChart`, donc les templates
# helm-chart*.yaml ne rendent rien ; ce fichier, lui, est rendu tel quel et
# appliqué par ArgoCD (app `grafana`, destination namespace `tools`).
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grafana-public
namespace: tools
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web
traefik.ingress.kubernetes.io/router.middlewares: kube-system-crowdsec@kubernetescrd
spec:
ingressClassName: traefik
rules:
- host: grafana.arcodange.fr
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: grafana
port:
number: 80
+123
View File
@@ -724,6 +724,14 @@ grafana: &grafana_config
editable: true
options:
path: /var/lib/grafana/dashboards/prospection
- name: 'kadans'
orgId: 1
folder: 'Kadans'
type: file
disableDeletion: false
editable: true
options:
path: /var/lib/grafana/dashboards/kadans
# - name: 'default'
# orgId: 1
@@ -778,6 +786,121 @@ grafana: &grafana_config
k8s-views-pods:
url: https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-views-pods.json
token: ''
# Jobs d'analyse Kadans : la file de la façade (pod kadans-jobs, annoté
# prometheus.io/*) + le worker du LAPTOP (cible statique kadans-worker-mac).
# Couleurs de STATUT sémantiques (pending/running/done/failed) — jamais
# recyclées pour de l'identité ; le sommeil du Mac est un ÉTAT, pas une
# alerte (« sa disponibilité n'impacte que la latence »).
kadans:
kadans-jobs:
json: |
{
"annotations": { "list": [] },
"editable": true,
"graphTooltip": 1,
"schemaVersion": 39,
"tags": ["kadans", "jobs", "arcodange"],
"title": "Kadans — jobs d'analyse (façade + worker Mac)",
"uid": "kadans-jobs-analyse",
"time": { "from": "now-24h", "to": "now" },
"panels": [
{
"type": "stat", "title": "Worker Mac", "id": 1,
"gridPos": { "h": 5, "w": 6, "x": 0, "y": 0 },
"targets": [{ "expr": "up{job=\"kadans-worker-mac\"}", "refId": "A" }],
"fieldConfig": { "defaults": {
"mappings": [
{ "type": "value", "options": { "1": { "text": "En écoute", "color": "green" }, "0": { "text": "Endormi", "color": "text" } } },
{ "type": "special", "options": { "match": "null", "result": { "text": "Endormi", "color": "text" } } }
],
"thresholds": { "mode": "absolute", "steps": [{ "color": "text", "value": null }] }
}, "overrides": [] },
"options": { "colorMode": "value", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"] } }
},
{
"type": "stat", "title": "Dernier poll (il y a)", "id": 2,
"gridPos": { "h": 5, "w": 6, "x": 6, "y": 0 },
"targets": [{ "expr": "time() - kadans_worker_dernier_poll_timestamp_seconds", "refId": "A" }],
"fieldConfig": { "defaults": {
"unit": "s", "decimals": 0,
"thresholds": { "mode": "absolute", "steps": [
{ "color": "green", "value": null }, { "color": "yellow", "value": 600 }
] }
}, "overrides": [] },
"options": { "colorMode": "value", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"] } }
},
{
"type": "stat", "title": "Jobs en attente", "id": 3,
"gridPos": { "h": 5, "w": 6, "x": 12, "y": 0 },
"targets": [{ "expr": "sum(kadans_jobs_jobs{statut=\"pending\"})", "refId": "A" }],
"fieldConfig": { "defaults": {
"thresholds": { "mode": "absolute", "steps": [{ "color": "text", "value": null }] }
}, "overrides": [] },
"options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }
},
{
"type": "stat", "title": "Plus ancien job en attente", "id": 4,
"gridPos": { "h": 5, "w": 6, "x": 18, "y": 0 },
"targets": [{ "expr": "kadans_jobs_plus_ancien_pending_age_seconds", "refId": "A" }],
"fieldConfig": { "defaults": {
"unit": "s", "decimals": 0,
"thresholds": { "mode": "absolute", "steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 3600 },
{ "color": "red", "value": 86400 }
] }
}, "overrides": [] },
"options": { "colorMode": "value", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"] } }
},
{
"type": "timeseries", "title": "La file, par statut", "id": 5,
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 5 },
"targets": [{ "expr": "kadans_jobs_jobs", "legendFormat": "{{statut}}", "refId": "A" }],
"fieldConfig": { "defaults": {
"custom": { "lineWidth": 2, "fillOpacity": 8, "pointSize": 4 }, "min": 0
}, "overrides": [
{ "matcher": { "id": "byName", "options": "pending" }, "properties": [{ "id": "color", "value": { "mode": "fixed", "fixedColor": "yellow" } }] },
{ "matcher": { "id": "byName", "options": "running" }, "properties": [{ "id": "color", "value": { "mode": "fixed", "fixedColor": "blue" } }] },
{ "matcher": { "id": "byName", "options": "done" }, "properties": [{ "id": "color", "value": { "mode": "fixed", "fixedColor": "green" } }] },
{ "matcher": { "id": "byName", "options": "failed" }, "properties": [{ "id": "color", "value": { "mode": "fixed", "fixedColor": "red" } }] }
] },
"options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }
},
{
"type": "timeseries", "title": "Publications (par heure)", "id": 6,
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 5 },
"targets": [{ "expr": "increase(kadans_jobs_publies_total[1h])", "legendFormat": "{{issue}}", "refId": "A" }],
"fieldConfig": { "defaults": {
"custom": { "lineWidth": 2, "fillOpacity": 8, "pointSize": 4 }, "min": 0, "decimals": 0
}, "overrides": [
{ "matcher": { "id": "byName", "options": "done" }, "properties": [{ "id": "color", "value": { "mode": "fixed", "fixedColor": "green" } }] },
{ "matcher": { "id": "byName", "options": "failed" }, "properties": [{ "id": "color", "value": { "mode": "fixed", "fixedColor": "red" } }] }
] },
"options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }
},
{
"type": "timeseries", "title": "Lanes exécutées par le worker (par heure)", "id": 7,
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 13 },
"targets": [{ "expr": "increase(kadans_worker_lanes_total[1h])", "legendFormat": "{{lane}} — {{issue}}", "refId": "A" }],
"fieldConfig": { "defaults": {
"custom": { "lineWidth": 2, "fillOpacity": 8, "pointSize": 4 }, "min": 0, "decimals": 0
}, "overrides": [] },
"options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }
},
{
"type": "timeseries", "title": "Durée moyenne d'une lane", "id": 8,
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 13 },
"targets": [{
"expr": "increase(kadans_worker_lane_duree_seconds_sum[1h]) / increase(kadans_worker_lane_duree_seconds_count[1h])",
"legendFormat": "{{lane}}", "refId": "A"
}],
"fieldConfig": { "defaults": {
"unit": "s", "custom": { "lineWidth": 2, "fillOpacity": 0, "pointSize": 5 }, "min": 0
}, "overrides": [] },
"options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }
}
]
}
prospection:
prospection:
json: |
+4
View File
@@ -19,6 +19,10 @@ applications = [
name = "plausible"
service_account_namespaces = ["tools"]
},
{
name = "minio"
service_account_namespaces = ["tools"]
},
{ name = "prospection" },
{ name = "kadans" },
]
+40
View File
@@ -0,0 +1,40 @@
# -----------------------------------------------------------------------------
# MinIO — stockage objet S3 du homelab (3 × Raspberry Pi 5, arm64).
#
# Pourquoi ici : c'est une BRIQUE PARTAGÉE, au même titre que pgbouncer ou
# clickhouse — le namespace `tools` en héberge le serveur ; les buckets, quotas
# et identifiants d'une application vivent, eux, avec cette application.
#
# Premier consommateur : Kadans (ADR-012 « MinIO local d'abord », ADR-013
# « OPFS local-first, MinIO/R2 = paliers payants »). La bascule vers Cloudflare
# R2 est prévue par l'ADR-012 aux seuils : 100+ utilisateurs actifs, > 10 To/mois
# de bande passante, ou dispersion géographique.
#
# Mode STANDALONE assumé : 3 nœuds, mais la donnée servie ici est DÉRIVÉE (le
# master d'une vidéo reste sur l'appareil de son propriétaire — ADR-018 du front) ;
# la redondance de Longhorn suffit, l'erasure coding distribué de MinIO coûterait
# de la RAM et des IOPS que des Pi n'ont pas à dépenser pour ça.
# -----------------------------------------------------------------------------
apiVersion: v2
name: minio
description: A Helm chart for Kubernetes
dependencies:
- name: tool
version: 0.1.0
repository: https://gitea.arcodange.lab/api/packages/arcodange-org/helm
- name: minio
version: 5.4.0
repository: https://charts.min.io/
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
version: 0.1.0
appVersion: "latest"
+54
View File
@@ -0,0 +1,54 @@
# MinIO — stockage objet S3 du homelab
Brique **partagée** du namespace `tools`, au même titre que pgbouncer ou
clickhouse. Le serveur vit ici ; les buckets, quotas et identifiants d'une
application vivent avec cette application.
## Premier consommateur : Kadans
- [ADR-012](https://gitea.arcodange.lab/arcodange/kadans/src/branch/main/docs/adr/012-video-storage-minio-first.md)
« MinIO local d'abord » — bascule vers Cloudflare R2 prévue aux seuils :
100+ utilisateurs actifs, > 10 To/mois, ou dispersion géographique.
- [ADR-013](https://gitea.arcodange.lab/arcodange/kadans/src/branch/main/docs/adr/013-video-storage-opfs-local-first.md)
le gratuit est **local-first** (la vidéo ne quitte pas l'appareil) ; MinIO sert
les **paliers payants**.
- [ADR-018](https://gitea.arcodange.lab/arcodange/kadans/src/branch/main/docs/adr/018-qualite-video-au-transfert.md)
ce qui transite est **dérivé** (aperçu 240p ~50 Ko, travail 360p ~3,4 Mo/min) —
le master reste chez l'utilisateur. D'où le dimensionnement ci-dessous.
## Ce que ce chart pose
| | |
|---|---|
| Mode | **standalone** (1 réplique) — la donnée est dérivée et Longhorn réplique déjà le volume ; l'erasure coding distribué coûterait de la RAM que des Pi 5 n'ont pas à dépenser pour ça |
| Volume | **50 Gi** sur `longhorn`**250 h de cours** au palier « travail ». ⚠ Longhorn réplique : compter **×3** sur la capacité du cluster avant d'augmenter |
| Ressources | requests 512 Mi / 100 m · limit 2 Gi — la limite protège les voisins de `tools`, pas MinIO |
| API S3 | `s3.arcodange.lab` (Traefik) |
| Console | `minio.arcodange.lab` (Traefik) |
| Bucket | `kadans-videos`, **privé** — l'accès passe par des URL signées (ADR-0002 du dossier produit) |
| Identifiants | **jamais dans le dépôt** : `iac/` les génère dans Vault (`kvv2/minio/config`), le Vault Secrets Operator les matérialise en secret `minio-config`, le chart les lit via `existingSecret` |
Le ServiceAccount du pod est nommé `minio` (et non le `minio-sa` par défaut du
chart amont) parce que le module Vault `app_roles` borne l'authentification au
SA portant le nom de l'app : un seul SA, rien à réconcilier.
## Première mise en service
L'ordre compte, et il compte **deux fois** :
1. **Workflow `Hashicorp Vault`** — MinIO doit d'abord figurer dans
`hashicorp-vault/iac/terraform.tfvars` (c'est fait) : c'est **là** que naît
le rôle CI `gitea_cicd_minio`, et non dans `minio/iac`. Sans cette étape,
le workflow MinIO échoue sur
`role "gitea_cicd_minio" could not be found` — il essaie de s'authentifier
avec un rôle que personne n'a encore créé.
2. **Workflow `MinIO`** — applique `minio/iac` : rôle Kubernetes pour le Vault
Secrets Operator, et **génération** du mot de passe root dans
`kvv2/minio/config`.
3. **ArgoCD** synchronise l'application (déclarée dans `chart/values.yaml`).
4. Vérifier : `kubectl -n tools get vaultstaticsecret minio` (secret
matérialisé) puis `kubectl -n tools get pods -l app=minio`.
> [!NOTE]
> Sans le secret `minio-config`, le pod ne démarre pas. C'est voulu — mieux
> vaut un pod en attente qu'un MinIO ouvert avec des identifiants par défaut.
+6
View File
@@ -0,0 +1,6 @@
terraform {
backend "gcs" {
bucket = "arcodange-tf"
prefix = "tools/minio/main"
}
}
+30
View File
@@ -0,0 +1,30 @@
# Rôle Vault + politique pour que le VaultAuth du chart puisse lire kvv2/minio/*
# (module partagé du repo même recette que plausible).
module "app_roles" {
source = "git::ssh://[email protected]:2222/arcodange-org/tools.git//hashicorp-vault/iac/modules/app_roles?depth=1&ref=main"
name = "minio"
service_account_namespaces = ["tools"]
}
# Identifiants ROOT de MinIO. Générés ici, jamais choisis à la main et jamais
# écrits dans le dépôt : le chart officiel les lit dans le secret k8s
# `minio-config` (clés rootUser / rootPassword), matérialisé par le Vault
# Secrets Operator depuis ce chemin.
resource "random_password" "root" {
length = 40
special = false # les outils S3 transportent mal certains caractères en URL
}
locals {
config = {
rootUser = "kadans-root"
rootPassword = random_password.root.result
}
}
resource "vault_kv_secret_v2" "config" {
mount = "kvv2"
name = "minio/config"
cas = 1
data_json = jsonencode(local.config)
}
+16
View File
@@ -0,0 +1,16 @@
terraform {
required_providers {
vault = {
source = "vault"
version = "4.4.0"
}
}
}
provider "vault" {
address = "https://vault.arcodange.lab"
auth_login_jwt { # TERRAFORM_VAULT_AUTH_JWT environment variable
mount = "gitea_jwt"
role = "gitea_cicd_minio"
}
}
+3
View File
@@ -0,0 +1,3 @@
{{- if eq .Values.tool.kind "HelmChart" -}}
{{- include "tool.helm-chart-config.tpl" . -}}
{{- end -}}
+3
View File
@@ -0,0 +1,3 @@
{{- if eq .Values.tool.kind "HelmChart" -}}
{{- include "tool.helm-chart.tpl" . -}}
{{- end -}}
+18
View File
@@ -0,0 +1,18 @@
{{- /* Identifiants MinIO : jamais dans le dépôt. Le Vault Secrets Operator
authentifie ce chart auprès de Vault (rôle créé par iac/) puis
matérialise le secret k8s consommé via `existingSecret`. Même recette
que plausible. */ -}}
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: minio
namespace: tools
spec:
vaultConnectionRef: default
method: kubernetes
mount: kubernetes
kubernetes:
role: minio
serviceAccount: minio
audiences:
- vault
+17
View File
@@ -0,0 +1,17 @@
{{- /* kvv2/minio/config → secret `minio-config` (clés rootUser / rootPassword,
les deux noms exigés par le chart officiel). Le mot de passe est généré
par Terraform (iac/), jamais choisi à la main. */ -}}
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: minio
namespace: tools
spec:
type: kv-v2
mount: kvv2
path: minio/config
destination:
name: minio-config
create: true
refreshAfter: 30s
vaultAuthRef: minio
+84
View File
@@ -0,0 +1,84 @@
minio: &minio_config
# Image officielle MinIO — multi-arch, arm64 inclus (les nœuds sont des Pi 5).
image:
repository: quay.io/minio/minio
pullPolicy: IfNotPresent
# STANDALONE : un seul serveur, un seul volume. La donnée servie ici est
# DÉRIVÉE (le master reste chez l'utilisateur) et Longhorn réplique déjà le
# volume ; l'erasure coding distribué coûterait de la RAM que les Pi n'ont pas
# à dépenser pour ça. `replicas` est ignoré en standalone.
mode: standalone
replicas: 1
# Le rôle Vault du module `app_roles` borne l'authentification au SERVICE
# ACCOUNT nommé comme l'app (`bound_service_account_names = [minio]`) : on
# aligne donc le SA du pod sur ce nom, plutôt que le « minio-sa » par défaut
# du chart amont — un SA pour le pod, le même pour Vault, rien à réconcilier.
serviceAccount:
create: true
name: minio
# Identifiants JAMAIS dans le dépôt : le secret est matérialisé par le
# Vault Secrets Operator depuis kvv2/minio/config (voir resources/ et iac/).
# Le chart lit `.data.rootUser` et `.data.rootPassword` de ce secret.
existingSecret: minio-config
persistence:
enabled: true
storageClass: longhorn
accessMode: ReadWriteOnce
# 50 Gi ≈ 250 heures de cours au palier « travail » de Kadans (360p,
# 3,4 Mo/min — ADR-018 du front). Longhorn réplique ce volume sur les
# nœuds : compter ×3 sur la capacité du cluster avant d'augmenter.
size: 50Gi
# Des Pi 5 à 8 Go partagés avec le reste de `tools` : on borne franchement.
# MinIO standalone est frugal ; la limite protège les voisins, pas MinIO.
resources:
requests:
memory: 512Mi
cpu: 100m
limits:
memory: 2Gi
# API S3 (les applications parlent ici).
ingress:
enabled: true
ingressClassName: traefik
path: /
hosts:
- s3.arcodange.lab
# Console d'administration (humains).
consoleIngress:
enabled: true
ingressClassName: traefik
path: /
hosts:
- minio.arcodange.lab
# Buckets créés au déploiement. `versioning: false` assumé : ces objets sont
# DÉRIVÉS et re-générables depuis le master local — versionner doublerait le
# stockage pour un filet dont on n'a pas besoin.
buckets:
- name: kadans-videos
policy: none # privé : l'accès passe par des URL signées (ADR-0002 du dossier)
purge: false
versioning: false
# Métriques : Prometheus (namespace `tools`) scrape déjà la façade et le
# laptop (ADR-0014 du dossier) — MinIO rejoint la même vue.
metrics:
serviceMonitor:
enabled: false # pas d'opérateur Prometheus ici : scrape par annotation
environment:
MINIO_PROMETHEUS_AUTH_TYPE: "public"
tool:
# kind: 'SubChart' or 'HelmChart', if subchart then uncomment Chart.yaml dependency, else comment and use tool library with helm chart template
kind: 'SubChart'
repo: https://charts.min.io/
chart: minio
version: 5.4.0
values: *minio_config
+102 -8
View File
@@ -819,6 +819,22 @@ prometheus: &prometheus_config
static_configs:
- targets:
- localhost:9090
# Le worker d'analyse Kadans sur le LAPTOP du fondateur (tier 1 VOLATILE,
# kadans-jobs cmd/worker, /metrics stdlib sur :9105). Un scrape en échec
# n'est PAS un incident : le Mac dort — `up == 0` raconte exactement ça
# (latence des analyses, jamais une indisponibilité produit). Ne PAS
# alerter sur cette cible.
# ⚠ IP DHCP : réserver 192.168.1.103 pour le Mac au routeur (ou remplacer
# par un nom résolvable du LAN) — sinon la cible dérive au renouvellement.
kadans-worker-mac:
enabled: true
job_name: "kadans-worker-mac"
static_configs:
- targets:
- 192.168.1.103:9105
labels:
tier: laptop
app: kadans-worker
kubernetes-api-servers:
enabled: true
job_name: ""
@@ -1121,9 +1137,9 @@ prometheus: &prometheus_config
alerting_rules.yml:
groups:
# Pipeline prospection (métriques poussées au Pushgateway job=prospection en fin de
# run). NB : la LIVRAISON des alertes (Alertmanager → Telegram/…) n'est pas encore
# câblée dans ce cluster — ces règles s'évaluent et sont visibles dans Prometheus
# /alerts + le dashboard Grafana « Prospection » (panneau Alertes actives).
# run). Livraison : Alertmanager → receiver telegram (chaîne testée live 2026-07-10,
# firing + resolved reçus). Visibles aussi dans Prometheus /alerts + le dashboard
# Grafana « Prospection » (panneau Alertes actives).
- name: prospection
rules:
- alert: ProspectionRunStale
@@ -1162,15 +1178,93 @@ prometheus: &prometheus_config
annotations:
summary: "Prospection — 0 offre (mission) collectée"
description: "Aucune offre au dernier run : collecte France Travail / Free-Work potentiellement cassée."
- alert: ProspectionBriefNotSent
expr: prospection_brief_telegram_pushed == 0
# Vidéo quotidienne : deux modes d'échec distincts, tous deux gardés contre le
# skip volontaire — metrics.py met brief_rendered=0 aussi quand l'étape est
# SAUTÉE (cadence/kill-switch), seul step_status distingue skip(2) d'erreur(0).
- alert: ProspectionBriefFailed
# PRODUCTION en échec : l'étape brief a tourné mais n'a pas rendu la vidéo.
expr: prospection_brief_rendered == 0 and on(job) prospection_step_status{step="brief"} != 2
for: 15m
labels:
severity: info
severity: warning
app: prospection
annotations:
summary: "Prospection — brief non poussé sur Telegram"
description: "Le brief vidéo n'a pas été diffusé sur Telegram au dernier run."
summary: "Prospection — la vidéo du brief quotidien n'a pas été produite"
description: "L'étape brief a échoué au dernier run (rendu vidéo KO : TTS/ffmpeg/PIL ou erreur amont). Voir les logs du CronJob prospection."
- alert: ProspectionBriefNotSent
# LIVRAISON en échec : vidéo produite mais pas poussée sur Telegram.
expr: prospection_brief_telegram_pushed == 0 and prospection_brief_rendered == 1
for: 15m
labels:
severity: warning
app: prospection
annotations:
summary: "Prospection — brief produit mais non poussé sur Telegram"
description: "La vidéo du brief a été rendue mais l'envoi Telegram a échoué (token/chat_id/API). Voir les logs du CronJob prospection."
# Santé du socle : être prévenu quand (ou juste avant que) le homelab tombe.
# Incident 2026-07-23 : build CI sans limites sur pi1 → RAM épuisée (0 swap),
# load15 >100, traefik + apiserver k3s affamés → tout *.arcodange.lab injoignable
# (Gitea compris, pourtant sain sur pi2). Prometheus vit sur pi3 et Alertmanager
# sur pi2 : cette chaîne d'alerte survit donc à la perte de pi1.
- name: homelab
rules:
- alert: NoeudInjoignable
# node-exporter ne répond plus : nœud éteint, réseau HS, ou surcharge telle
# que plus rien n'y répond (le cas de l'incident).
expr: up{job="kubernetes-service-endpoints", app_kubernetes_io_name="prometheus-node-exporter"} == 0
for: 3m
labels:
severity: critical
app: homelab
annotations:
summary: "Homelab — {{ $labels.node }} est injoignable"
description: "node-exporter de {{ $labels.node }} ({{ $labels.instance }}) ne répond plus depuis 3 min : nœud down ou en surcharge sévère."
- alert: IngressLabIndisponible
# Plus aucun replica traefik dispo — ou kube-state-metrics muet (il vit sur
# pi1 : quand pi1 tombe, la métrique disparaît au lieu de passer à 0).
expr: >-
kube_deployment_status_replicas_available{namespace="kube-system",deployment="traefik"} < 1
or absent(kube_deployment_status_replicas_available{namespace="kube-system",deployment="traefik"})
for: 3m
labels:
severity: critical
app: homelab
annotations:
summary: "Homelab — ingress traefik indisponible : *.arcodange.lab est HS"
description: "Aucun replica traefik disponible (ou métrique absente = kube-state-metrics muet). Gitea, ArgoCD, Grafana, Vault… sont injoignables via leurs URLs .lab. Gitea reste accessible en direct : http://192.168.1.202:3000."
- alert: NoeudPressionMemoire
# Précurseur direct de l'incident : <500 Mo dispo sur un Pi 8 Go — sans
# swap, le kernel part en thrash bien avant d'atteindre 0.
expr: node_memory_MemAvailable_bytes < 500 * 1024 * 1024
for: 5m
labels:
severity: warning
app: homelab
annotations:
summary: "Homelab — mémoire critique sur {{ $labels.node }}"
description: "{{ $labels.node }} n'a plus que {{ $value | humanize1024 }}B de mémoire disponible depuis 5 min : risque imminent de thrash (pas de swap sur les Pis). Suspect n°1 : un job CI trop gourmand."
- alert: NoeudEnSurcharge
expr: node_load15 > 8
for: 10m
labels:
severity: warning
app: homelab
annotations:
summary: "Homelab — {{ $labels.node }} en surcharge (load15 = {{ $value | humanize }})"
description: "load15 > 8 depuis 10 min sur {{ $labels.node }} (4 cœurs) : quelque chose sature la machine, l'ingress et l'API k3s sont en danger si c'est pi1."
- alert: CertificatNonRenouvele
# Les certs .lab durent 24 h et cert-manager renouvelle à ~16 h d'âge : s'il
# reste <4 h, le renouvellement a raté plusieurs fois (incident 2026-07-24 :
# step-issuer ne résolvait plus ssl-ca.arcodange.lab, wildcard expiré au matin
# → « le https ne fonctionne plus » sur tout *.arcodange.lab).
expr: certmanager_certificate_expiration_timestamp_seconds - time() < 4 * 3600
for: 15m
labels:
severity: critical
app: homelab
annotations:
summary: "Homelab — cert {{ $labels.namespace }}/{{ $labels.name }} expire dans {{ $value | humanizeDuration }}"
description: "Le renouvellement automatique (cert-manager → step-issuer → step-ca) est en échec. Vérifier : kubectl get certificaterequest -A, logs step-issuer (résolution DNS de ssl-ca.arcodange.lab), santé de step-ca sur pi1:8443."
# groups:
# - name: Instances
# rules: