Merge remote-tracking branch 'origin/dev' into feat/reconciliation-dag
This commit is contained in:
@@ -51,8 +51,8 @@ flowchart TB
|
||||
api["API FastAPI<br/>apps/backend"]
|
||||
db[("PostgreSQL 17<br/>TimescaleDB")]
|
||||
airflow["Airflow<br/>etl/airflow"]
|
||||
prom["Prometheus"]
|
||||
grafana["Grafana"]
|
||||
prom["Prometheus<br/>profil monitoring"]
|
||||
grafana["Grafana<br/>profil monitoring"]
|
||||
end
|
||||
|
||||
navigateur --> proxy
|
||||
@@ -61,9 +61,9 @@ flowchart TB
|
||||
front -.-> api
|
||||
api --> db
|
||||
airflow --> db
|
||||
prom -.-> api
|
||||
grafana -.-> db
|
||||
grafana -.-> prom
|
||||
prom --> api
|
||||
grafana --> db
|
||||
grafana --> prom
|
||||
```
|
||||
|
||||
Le lien `front -.-> api` reste en pointillé : le frontend appelle bien une API, mais un
|
||||
@@ -82,8 +82,11 @@ CSV plutôt que de laisser les deux sources dupliquer silencieusement un même i
|
||||
pipeline ML déduplique par construction (`DISTINCT ON`, source `csv` préférée) au cas où un
|
||||
recouvrement se produirait malgré tout — voir [40-data.md](40-data.md).
|
||||
|
||||
Le lien `prom -.-> api` de même : l'API expose bien `/metrics` au format Prometheus, mais aucun
|
||||
collecteur ne vient le lire.
|
||||
Les liens de la supervision sont en trait plein depuis le 23/09 (issue #26) : Prometheus scrute
|
||||
`/metrics` avec un jeton, Grafana lit Prometheus et, par un rôle en lecture seule, les tables
|
||||
métier de TimescaleDB. Ils tournent en prod sous le profil Compose `monitoring`, à la demande
|
||||
ailleurs ([ADR 0016](../adr/0016-supervision-en-profil-compose.md),
|
||||
[60-observabilite.md](60-observabilite.md)).
|
||||
|
||||
## État de la stack
|
||||
|
||||
@@ -94,9 +97,9 @@ collecteur ne vient le lire.
|
||||
| Base | PostgreSQL 17 + TimescaleDB | `db` | `Fait` | Bootstrap de l'extension, base de test, chaîne Alembic. Schéma applicatif créé (`site`, `dataset`, `reading` en hypertable, `prediction`, `alert`, `recommendation`) |
|
||||
| ML | LightGBM, MLflow | `ml` | `En cours` | Pipeline d'entraînement et de scoring (`enervision_ml.train`/`.score`, features par lags/moyennes glissantes partagées entre les deux, baseline de persistance saisonnière, suivi MLflow local), exposé en lecture via `GET /predictions`, orchestré par Airflow (`ml_train`/`ml_score`). Voir [ADR 0005](../adr/0005-modele-prediction-lightgbm.md) et [ML-START.md](../ML-START.md). Surveillance de dérive livrée côté backend (`app.monitoring.drift`, table `drift_report`, `GET /monitoring/drift`, DAG `derive`), voir [ADR 0013](../adr/0013-surveillance-de-derive-dans-le-backend.md) |
|
||||
| Infra | Docker Compose, Nginx, Terraform, k3s single-node | `infra`, `docker-compose.prod.yml` | `En cours` | Reverse proxy et overlay de déploiement écrits et validés, jamais lancés sur le serveur ([ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md)). Provisionnement de la VM par Terraform, qui installe Docker, prépare les deux environnements et enregistre le runner, jamais appliqué ([ADR 0010](../adr/0010-terraform-provisionne-github-actions-deploie.md)). Module d'installation k3s jamais appliqué, aucune ressource Kubernetes déclarée |
|
||||
| Monitoring | Prometheus, Grafana, Alertmanager | `monitoring` | `Cible` | Rien, hors le `/metrics` exposé par l'API |
|
||||
| Monitoring | Prometheus, Grafana, Alertmanager | `monitoring` | `Fait` | Profil Compose `monitoring`, actif en prod : Prometheus et trois exporteurs (PostgreSQL, hôte, conteneurs), neuf règles d'alerte testées par `promtool`, Alertmanager vers Mailpit, trois tableaux de bord Grafana provisionnés. Voir [60-observabilite.md](60-observabilite.md) |
|
||||
| ETL | Apache Airflow | `etl/airflow` | `En cours` | Webserver et scheduler avec LocalExecutor via Docker Compose, sur une base PostgreSQL dédiée. Six DAGs en sous-processus `uv run` : `ml_train`, `ml_score`, `alertes`, `historical_import`, `mock_api_import` et `derive` (quotidien, surveillance de dérive). L'import historique reste manuel et l'import API Mock s'exécute chaque heure. Réconciliation entre les deux sources (issue #15) : trou temporel accepté, recouvrement refusé à l'ingestion et dédupliqué en défense côté ML, voir [40-data.md](40-data.md). |
|
||||
| CI/CD | GitHub Actions | `.github/workflows` | `En cours` | 7 workflows, 19 jobs : lint, typage, tests avec seuil de couverture bloquant, tests d'intégration sur TimescaleDB réel, audit de dépendances, SAST Bandit, quality gate SonarCloud, intégrité des DAGs Airflow, formatage et validation du Terraform. Déploiement continu vers la VM ENI écrit par `deploy.yml`, `dev` en recette et `main` en production après approbation ([ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md)), mais jamais exécuté : la machine n'est pas provisionnée et le runner n'y est pas enregistré. Détail dans [50-cicd.md](50-cicd.md) |
|
||||
| CI/CD | GitHub Actions | `.github/workflows` | `En cours` | Un orchestrateur `ci.yml` qui n'appelle que les composants modifiés ([ADR 0014](../adr/0014-pipeline-ci-unique-et-deploiement-conditionne.md)) : lint, typage, tests avec seuil de couverture bloquant, tests d'intégration sur TimescaleDB réel, audit de dépendances, SAST Bandit, quality gate SonarCloud, intégrité des DAGs Airflow, Terraform, Compose et supervision, parcours Playwright et tirs k6 contre la stack de prod ([ADR 0015](../adr/0015-tests-e2e-et-de-charge-contre-la-stack-compose.md)). Déploiement vers la VM ENI par `deploy.yml`, appelé une fois « CI ok » vert, `dev` en recette et `main` en production après approbation ([ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md)), mais jamais exécuté : le runner n'est pas enregistré sur la machine. Détail dans [50-cicd.md](50-cicd.md) |
|
||||
|
||||
## Flux bout en bout
|
||||
|
||||
@@ -155,7 +158,7 @@ consolidée.
|
||||
- **Caviardage des journaux** : jetons, empreintes Argon2, mots de passe et cookies sont
|
||||
expurgés avant écriture.
|
||||
- **Documentation interactive fermée** en préproduction et en production, `/metrics` derrière un
|
||||
jeton facultatif, sonde de disponibilité qui ne publie plus la version de TimescaleDB.
|
||||
jeton, exigé dès que la supervision tourne, sonde de disponibilité qui ne publie plus la version de TimescaleDB.
|
||||
- **CI backend bloquante** : format, lint, typage strict et tests avec seuil de couverture.
|
||||
- **Conteneur backend non-root**, déclaré dans `apps/backend/Dockerfile`.
|
||||
- **Terminaison TLS au frontal** : un reverse proxy Nginx est le seul service publié, il redirige
|
||||
|
||||
@@ -37,6 +37,8 @@ flowchart TB
|
||||
|---|---|---|
|
||||
| `db` | `timescale/timescaledb-ha:pg17` | Publié sur **5433** côté hôte, 5432 souvent déjà pris. `healthcheck` `pg_isready`, 12 tentatives, `start_period` 40s |
|
||||
| `backend` | Construite depuis `apps/backend` | `depends_on: db, condition: service_healthy`. **N'embarque pas le source** : toute modification impose `docker compose up -d --build backend` |
|
||||
| `prometheus`, `alertmanager`, `grafana`, exporteurs | Images épinglées par tag | Profil `monitoring`, jamais démarrés par `make dev`. `make monitoring-up` les lance en `--no-deps`. Voir [60-observabilite.md](60-observabilite.md) |
|
||||
| `k6` | `grafana/k6` | Profil `load`, lancé par `make load-*` le temps d'un tir, sur le réseau du projet. Voir [`tests/load/README.md`](../../tests/load/README.md) |
|
||||
|
||||
**La boucle de développement n'utilise pas le service `backend`.** `make db-up` puis `make dev` :
|
||||
seule la base tourne en conteneur, l'API et `ng serve` tournent sur le poste avec le rechargement
|
||||
@@ -201,11 +203,12 @@ flowchart LR
|
||||
navigateur["Navigateur"]
|
||||
|
||||
subgraph machine["Machine on-premise"]
|
||||
proxy["service proxy<br/>nginx:1.28-alpine<br/>:80 et :443"]
|
||||
proxy["service proxy<br/>nginx:1.31-alpine<br/>:80 et :443"]
|
||||
front["service frontend<br/>nginx statique :3000"]
|
||||
api["service backend<br/>uvicorn :8000"]
|
||||
db[("service db<br/>:5432")]
|
||||
mail["service mailpit"]
|
||||
sup["profil monitoring<br/>Prometheus, Alertmanager, Grafana"]
|
||||
end
|
||||
|
||||
navigateur -->|"HTTPS"| proxy
|
||||
@@ -213,6 +216,9 @@ flowchart LR
|
||||
proxy -->|"/api/"| api
|
||||
api --> db
|
||||
api --> mail
|
||||
sup -->|"/metrics, jeton"| api
|
||||
sup -->|"rôle supervision, lecture seule"| db
|
||||
sup -->|"alertes par courriel"| mail
|
||||
```
|
||||
|
||||
Le proxy est **le seul service à publier des ports** sur le réseau. Backend et frontend ne sont
|
||||
@@ -227,27 +233,36 @@ Deux conséquences se propagent jusqu'à l'application, et elles ne se devinent
|
||||
- `APP_TRUST_PROXY_HEADERS` passe à vrai en même temps, sinon la limitation de débit par IP
|
||||
compte sur l'IP du proxy et devient globale.
|
||||
|
||||
### Deux environnements sur la même machine
|
||||
### Trois environnements sur la même machine
|
||||
|
||||
Statut : `En cours`, la machine n'étant pas encore provisionnée. Décision et motifs dans
|
||||
l'[ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md).
|
||||
La VM `eadl-2025-nantes-g3` portera la recette et la production, chacune dans son clone du dépôt,
|
||||
son `.env` et son projet Compose. Le nom de projet préfixe volumes, réseau et conteneurs : rien
|
||||
n'est partagé. `scripts/provision-host.sh` prépare les deux dossiers, génère les secrets et les
|
||||
certificats, et ne démarre rien.
|
||||
Statut : `En cours`. Décision et motifs dans
|
||||
l'[ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md), étendue à un troisième
|
||||
environnement par l'[ADR 0017](../adr/0017-environnement-dev-a-la-demande.md) ; noms,
|
||||
certificats et frontal sans port dans l'[ADR 0018](../adr/0018-noms-publics-certificats-dns01-et-frontal-sni.md).
|
||||
La VM `eadl-2025-nantes-g3` porte le développement, la recette et la production, chacun dans son
|
||||
clone du dépôt, son `.env` et son projet Compose. Le nom de projet préfixe volumes, réseau et
|
||||
conteneurs : rien n'est partagé. `scripts/provision-host.sh` prépare les trois dossiers, génère
|
||||
les secrets et les certificats, et ne démarre rien.
|
||||
|
||||
| | Recette | Production |
|
||||
|---|---|---|
|
||||
| Branche, environnement GitHub | `dev`, `rec` | `main`, `prod` |
|
||||
| Dossier, projet Compose | `/srv/enervision/rec`, `enervision-rec` | `/srv/enervision/prod`, `enervision-prod` |
|
||||
| URL | `https://rec.enervision.local:8443` | `https://enervision.local` |
|
||||
| Proxy HTTP, HTTPS | `127.0.0.1:8081`, `8443` | `80`, `443` |
|
||||
| PostgreSQL, Mailpit, Airflow, sur `127.0.0.1` | `5434`, `8026`, `8082` | `5433`, `8025`, `8080` |
|
||||
| | Développement | Recette | Production |
|
||||
|---|---|---|---|
|
||||
| Branche, environnement GitHub | toute branche lancée à la main, `dev` | `dev`, `rec` | `main`, `prod` |
|
||||
| Dossier, projet Compose | `/srv/enervision/dev`, `enervision-dev` | `/srv/enervision/rec`, `enervision-rec` | `/srv/enervision/prod`, `enervision-prod` |
|
||||
| URL | `https://dev.enervision-g3.dynv6.net` | `https://rec.enervision-g3.dynv6.net` | `https://prod.enervision-g3.dynv6.net` |
|
||||
| Proxy HTTP, HTTPS, PROXY protocol, sur `127.0.0.1` | `8083`, `9443`, `9444` | `8081`, `8443`, `8444` | `10080`, `10443`, `10444` |
|
||||
| PostgreSQL, Mailpit, Airflow, sur `127.0.0.1` | `5435`, `8027`, `8084` | `5434`, `8026`, `8082` | `5433`, `8025`, `8080` |
|
||||
| Supervision (profil `monitoring`) | à la demande, `make monitoring-up` | à la demande, `make monitoring-up` | active, `COMPOSE_PROFILES=monitoring` |
|
||||
| Grafana, Prometheus, Alertmanager, sur `127.0.0.1` | `3003`, `9092`, `9095` | `3002`, `9091`, `9094` | `3001`, `9090`, `9093` |
|
||||
|
||||
Les deux noms d'hôte visent la même IP, à déclarer dans le `/etc/hosts` des postes. Deux noms
|
||||
distincts sont nécessaires : le cookie `__Secure-ev_refresh` est posé par hôte, pas par port.
|
||||
La redirection HTTP de la recette est ramenée sur la boucle locale parce que la configuration
|
||||
Nginx renvoie vers `https://$host` sans port, c'est-à-dire vers la production.
|
||||
Les trois noms sont publics chez dynv6 et visent l'IP privée de la VM : rien à déclarer sur
|
||||
les postes du réseau de l'école, et rien n'est joignable hors de ce réseau. Trois noms distincts
|
||||
sont nécessaires : le cookie `__Secure-ev_refresh` est posé par hôte, pas par port.
|
||||
|
||||
Aucune stack ne publie hors de la boucle locale. Le frontal `infra/front`, sur le réseau de
|
||||
l'hôte, écoute 80 et 443 : il redirige le premier, et aiguille le second d'après le nom demandé
|
||||
(SNI) vers l'écouteur PROXY protocol de la stack visée, sans déchiffrer le TLS. Chaque stack
|
||||
garde son certificat Let's Encrypt, obtenu par défi DNS-01 (`make tls-dns01`) et renouvelé à
|
||||
chaque déploiement ainsi que chaque nuit par `/etc/cron.d/enervision-tls`.
|
||||
|
||||
Le déploiement est décrit dans [50-cicd.md](50-cicd.md) : un runner GitHub Actions installé sur
|
||||
la VM aligne le dossier sur la branche poussée et lance `make stack-up`.
|
||||
@@ -267,7 +282,7 @@ sequenceDiagram
|
||||
|
||||
TF->>VM: SSH, get.docker.com puis docker compose version
|
||||
TF->>VM: copie et exécute scripts/provision-host.sh
|
||||
VM->>VM: deux clones, deux .env, deux certificats
|
||||
VM->>VM: trois clones, trois .env, trois certificats
|
||||
TF->>VM: installe actions-runner, config.sh, svc.sh
|
||||
VM->>GH: le runner s'enregistre avec le label eni-g3
|
||||
```
|
||||
@@ -353,11 +368,13 @@ Ces arbitrages sont pris. Ils ne vivaient jusqu'ici que dans des commentaires de
|
||||
| API | `8000` | Identique en conteneur et hors conteneur |
|
||||
| Frontend, `ng serve` | `4200` | Boucle de développement. Valeur par défaut d'`APP_CORS_ORIGINS` |
|
||||
| Frontend en conteneur | `3000` | Ce qu'écoute le nginx de l'image, en conteneur comme côté hôte |
|
||||
| Reverse proxy | `80` et `443` | Les seuls ports publiés par `docker-compose.prod.yml`, via `PROXY_HTTP_PORT` et `PROXY_HTTPS_PORT`. 80 ne sert que la redirection et le défi ACME. La recette publie `8443` et `127.0.0.1:8081` |
|
||||
| Reverse proxy | `80` et `443`, plus `4443` | Les seuls ports publiés par `docker-compose.prod.yml`, via `PROXY_HTTP_PORT`, `PROXY_HTTPS_PORT` et `PROXY_FRONT_PORT`. 80 ne sert que la redirection et le défi ACME ; 4443 n'accepte que le PROXY protocol du frontal. Sur la VM, tous sur `127.0.0.1` |
|
||||
| Frontal SNI de la VM | `80` et `443` de l'hôte | `infra/front`, seul composant exposé sur le réseau de l'école (ADR 0018) |
|
||||
| SSH du serveur | `22` par défaut | `ssh_port`, redéfinissable |
|
||||
| Base applicative | `enervision` | Variable `POSTGRES_DB` |
|
||||
| Base de test | `enervision_test` | Créée par `db/init/110-test-database.sql`, nom attendu en dur par `apps/backend/tests/conftest.py` |
|
||||
| Base de métadonnées Airflow | `airflow` | Créée par `db/init/120-airflow-database.sql`, même conteneur `db` |
|
||||
| Grafana, Prometheus, Alertmanager | `3001`, `9090`, `9093` | Sur `127.0.0.1` seulement, profil `monitoring`. `GRAFANA_PORT`, `PROMETHEUS_PORT`, `ALERTMANAGER_PORT`. 3000 est pris par le frontend |
|
||||
| API server Airflow | `8080` | `make airflow-up`. Api-server, scheduler et dag-processor ne publient que ce port ; les tâches (`LocalExecutor`) tournent côté scheduler, sans port propre |
|
||||
|
||||
## Le trou vers k3s
|
||||
|
||||
@@ -103,7 +103,7 @@ démarre ne prouve rien sur la base, la première connexion réelle a lieu au pr
|
||||
| `APP_LOGIN_MAX_FAILURES_PER_IDENTIFIER` | `50` | Signature d'une attaque distribuée |
|
||||
| `APP_TRUST_PROXY_HEADERS` | `false` | À vrai derrière un proxy, sinon le compteur par IP devient global |
|
||||
| `APP_EXPOSE_API_DOCS` | déduit | Faux en `staging` et `prod` si non renseigné |
|
||||
| `APP_METRICS_TOKEN` | absent | Si présent, `/metrics` exige `Authorization: Bearer` |
|
||||
| `APP_METRICS_TOKEN` | absent | Si présent et non vide, `/metrics` exige `Authorization: Bearer`. Vide vaut absent |
|
||||
|
||||
Cinq gardes refusent de démarrer plutôt que de laisser passer une erreur silencieuse :
|
||||
secret de moins de 32 caractères ou laissé à sa valeur d'exemple, `debug` en `staging` ou
|
||||
@@ -440,7 +440,17 @@ Le reste, par ordre de surface :
|
||||
|
||||
- Journalisation par `dictConfig` : format console en développement, JSON dès `APP_ENV=prod`.
|
||||
`sqlalchemy.engine` est forcé à `WARNING` pour ne pas noyer les journaux.
|
||||
- `/metrics` au format Prometheus. **Aucun collecteur ne le lit** : `monitoring/` est vide.
|
||||
- `/metrics` au format Prometheus (`prometheus-fastapi-instrumentator`), scruté toutes les 15 s
|
||||
par Prometheus sous le profil `monitoring` ([60-observabilite.md](60-observabilite.md)).
|
||||
- **Séries publiées.** `http_requests_total` par route, méthode et classe de statut, et
|
||||
`http_request_duration_seconds` par route, avec des seaux de 50 ms à 2,5 s autour du seuil
|
||||
de charge de 500 ms (ADR 0015). Aussi `http_request_duration_highr_seconds`, fin mais sans
|
||||
libellé de route, et les métriques du processus.
|
||||
- **Exclusions.** Les sondes `/health/*` et `/metrics` lui-même sont exclus : la sonde Docker
|
||||
de 30 s fausserait débit et latences.
|
||||
- **Un registre par application** (`_registre_de_metriques()` dans `main.py`). Le registre
|
||||
global de `prometheus_client` n'accepte chaque métrique qu'une fois : toute application créée
|
||||
après la première, dans les tests notamment, ne mesurait rien.
|
||||
|
||||
## Tests
|
||||
|
||||
|
||||
+141
-107
@@ -10,8 +10,9 @@ vérifié, ce qui bloque, et ce qui ne l'est pas.
|
||||
|
||||
Le **D** de CI/CD est écrit depuis le 21/09 : `deploy.yml` déploie `dev` en recette et `main` en
|
||||
production sur la VM de l'école, par un runner auto-hébergé (issue #21,
|
||||
[ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md)). Il n'a encore rien
|
||||
déployé : la machine n'est pas provisionnée et le runner n'y est pas enregistré. Statut à
|
||||
[ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md)). Depuis le 23/09, il ne part
|
||||
plus qu'une fois la CI du commit verte ([ADR 0014](../adr/0014-pipeline-ci-unique-et-deploiement-conditionne.md)).
|
||||
Il n'a encore rien déployé : le runner n'est pas enregistré sur la machine. Statut à
|
||||
basculer sur `Fait` au premier déploiement vert. Sa limite, nommée ici plutôt que découverte en
|
||||
soutenance : les images sont construites sur la machine à chaque déploiement, aucun artefact
|
||||
n'est publié puis promu d'un environnement à l'autre.
|
||||
@@ -24,95 +25,67 @@ GitHub Actions déploie ; aucun des deux ne fait le travail de l'autre.
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
`ci.yml` est le seul point d'entrée des PR et des push sur `dev` et `main`. Il appelle les
|
||||
workflows de composant, qui n'ont plus de déclencheur propre, selon les fichiers modifiés
|
||||
([ADR 0014](../adr/0014-pipeline-ci-unique-et-deploiement-conditionne.md)).
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
push["push ou pull_request"]
|
||||
evt["pull_request, ou push sur dev et main"]
|
||||
changes["changes<br/>paths-filter : composants touchés"]
|
||||
|
||||
subgraph back["Backend · .github/workflows/backend.yml"]
|
||||
bv["verification<br/>ruff, mypy, pytest --cov-fail-under=85"]
|
||||
bi["integration<br/>TimescaleDB réel + alembic upgrade head"]
|
||||
bd["security-audit<br/>uv export | pip-audit"]
|
||||
bs["sast<br/>bandit"]
|
||||
subgraph comp["Workflows de composant (workflow_call)"]
|
||||
back["backend.yml<br/>lint, typage, tests ≥ 85 %, intégration, pip-audit, bandit"]
|
||||
front["frontend.yml<br/>build et tests, npm audit"]
|
||||
mlw["ml.yml<br/>lint, typage, tests, ML ↔ DB, chaîne ML → API, bandit"]
|
||||
afw["airflow.yml<br/>intégrité des DAGs, image"]
|
||||
infw["infra.yml<br/>Terraform, Compose et supervision, actionlint"]
|
||||
e2e["e2e.yml<br/>stack de prod, Playwright, k6 smoke et limitation"]
|
||||
end
|
||||
|
||||
subgraph front["Frontend · frontend.yml"]
|
||||
fb["build<br/>npm ci, npm run build"]
|
||||
ft["test<br/>couverture lcov"]
|
||||
fd["security-audit<br/>npm audit --audit-level=high"]
|
||||
end
|
||||
sonar["sonar<br/>reprend les couvertures du run"]
|
||||
ok["CI ok<br/>seul check à exiger"]
|
||||
dep["deploy.yml<br/>runner eni-g3, rec ou prod"]
|
||||
|
||||
subgraph mlw["ML · ml.yml"]
|
||||
mv["verification<br/>ruff, mypy, pytest"]
|
||||
ms["sast<br/>bandit"]
|
||||
end
|
||||
evt --> changes --> back & front & mlw & afw & infw & e2e
|
||||
back & front & mlw --> sonar
|
||||
back & front & mlw & afw & infw & e2e & sonar --> ok
|
||||
ok -->|"push sur dev ou main"| dep
|
||||
|
||||
subgraph afw["Airflow · airflow.yml"]
|
||||
av["verification<br/>ruff, intégrité des DAGs"]
|
||||
ab["image<br/>construction de l'image"]
|
||||
end
|
||||
|
||||
subgraph infw["Infra · infra.yml"]
|
||||
it["terraform<br/>fmt -check, init et validate par racine"]
|
||||
end
|
||||
|
||||
subgraph sq["SonarQube · sonarqube.yml"]
|
||||
sb1["build-front / test-front"]
|
||||
sb2["build-back / test-back"]
|
||||
sb3["test-ml"]
|
||||
sscan["sonarqube<br/>quality gate SonarCloud"]
|
||||
end
|
||||
|
||||
push --> bv & bi & bd & bs
|
||||
push --> fb --> ft
|
||||
push --> fd
|
||||
push --> mv & ms
|
||||
push --> av & ab
|
||||
push --> it
|
||||
push --> sb1 & sb2 & sb3 --> sscan
|
||||
|
||||
subgraph cd["Déploiement · deploy.yml"]
|
||||
dep["deploy<br/>runner eni-g3, environnement rec ou prod"]
|
||||
end
|
||||
|
||||
push -->|"push sur dev ou main"| dep
|
||||
|
||||
planifie["chaque lundi 3h UTC,<br/>ou à la main"]
|
||||
subgraph dastw["DAST · dast.yml"]
|
||||
zscan["zap<br/>seed + scan actif OWASP ZAP"]
|
||||
end
|
||||
|
||||
planifie --> zscan
|
||||
push -->|"PR sur dast.yml<br/>ou dast-token.sh"| zscan
|
||||
planifie["chaque lundi 3h UTC, à la main,<br/>ou PR sur ses fichiers"]
|
||||
dast["dast.yml<br/>seed + scan actif OWASP ZAP"]
|
||||
planifie --> dast
|
||||
```
|
||||
|
||||
## Déclenchement
|
||||
|
||||
Les six workflows hébergés par GitHub qui vérifient le code se déclenchent sur `push` **et** sur
|
||||
`pull_request`, filtrés par **chemin** : `backend.yml` sur `apps/backend/**`, `frontend.yml` sur
|
||||
`apps/frontend/**`, `ml.yml` sur `ml/**`, `infra.yml` sur `infra/terraform/**`, `airflow.yml` sur
|
||||
`etl/airflow/**` **plus des chemins de `ml/` et de `apps/backend/`**, chacun incluant son propre
|
||||
fichier de workflow dans le filtre pour qu'une modification du pipeline déclenche le pipeline.
|
||||
**Sur une PR**, le job `changes` lit la liste des fichiers modifiés par l'API GitHub
|
||||
(`dorny/paths-filter`, épinglé sur un SHA) et chaque composant n'est appelé que si son filtre
|
||||
vaut vrai. Une PR de documentation ne joue que `changes` et `CI ok`. Modifier `ci.yml` rejoue
|
||||
tout.
|
||||
|
||||
`dast.yml` s'en écarte volontairement (détail dans sa propre section plus bas) : aucun
|
||||
déclenchement sur `push`, seulement `workflow_dispatch`, une planification hebdomadaire, et
|
||||
`pull_request` restreint à ses deux seuls fichiers. Un scan actif est trop long pour tourner à
|
||||
chaque commit.
|
||||
**Sur un push vers `dev` ou `main`**, tous les filtres valent vrai. C'est le moment où l'analyse
|
||||
Sonar doit couvrir tout le dépôt, et paths-filter comparerait sinon le push à sa base de fusion
|
||||
avec `main`, en retard de 80 commits. Une branche de travail ne déclenche plus rien par un push :
|
||||
la CI part de sa PR, une seule fois par commit.
|
||||
|
||||
Le filtre d'`airflow.yml` mérite un mot : il inclut `ml/pyproject.toml`, `ml/uv.lock`,
|
||||
`ml/enervision_ml/**`, `apps/backend/pyproject.toml`, `apps/backend/uv.lock` et
|
||||
`apps/backend/app/**` parce que l'image Airflow copie le code et les dépendances des deux
|
||||
modules : celles du ML pour `ml_train`/`ml_score`, celles du backend depuis que le DAG `alertes`
|
||||
y exécute les commandes de détection ([ADR 0008](../adr/0008-airflow-execute-le-code-du-backend.md)).
|
||||
Une modification de l'un ou l'autre peut donc casser la construction de cette image, et le filtre
|
||||
le voit.
|
||||
Deux filtres écoutent plus que leur dossier, parce que ce qu'ils testent dépend d'autres modules :
|
||||
|
||||
**Piège à connaître** : il n'y a **aucun filtre de branche**. Une branche de travail déclenche la
|
||||
CI complète à chaque push, et un merge vers n'importe quelle branche la déclenche aussi. C'est
|
||||
délibéré pendant le projet (retour au plus tôt, et la CI tournera sur `main` dès la remontée sans
|
||||
rien changer), mais ce serait à borner sur un dépôt à forte fréquence de push.
|
||||
- `airflow` inclut `ml/pyproject.toml`, `ml/uv.lock`, `ml/enervision_ml/**`,
|
||||
`apps/backend/pyproject.toml`, `apps/backend/uv.lock` et `apps/backend/app/**`. L'image
|
||||
Airflow copie le code et les dépendances des deux modules
|
||||
([ADR 0008](../adr/0008-airflow-execute-le-code-du-backend.md)), et une modification de l'un
|
||||
ou de l'autre peut casser sa construction.
|
||||
- `e2e` inclut le frontend, l'API, ses migrations et son Dockerfile, le proxy, les fichiers
|
||||
Compose, `db/`, `tests/` et les scripts qu'il appelle : tout ce qui change un parcours.
|
||||
|
||||
`backend.yml`, `ml.yml` et `airflow.yml` déclarent en plus un groupe de concurrence par référence
|
||||
git avec `cancel-in-progress`, ce qui annule un run devenu obsolète par un push plus récent.
|
||||
`dast.yml` reste hors de l'orchestrateur : un scan actif est trop long pour chaque PR. Il se
|
||||
lance à la main, chaque lundi, et sur une PR qui modifie le scan, son jeu de données ou ses
|
||||
comptes.
|
||||
|
||||
Le groupe de concurrence de `ci.yml` annule le run d'une PR devenu obsolète par un push plus
|
||||
récent. Pour un push sur `dev` ou `main`, le groupe est le SHA et rien n'est annulé : un run
|
||||
coupé en plein `make stack-up` laisserait la stack à moitié redémarrée.
|
||||
|
||||
**Piège de version** : `etl/airflow` tourne en **Python 3.12** et non 3.14 : c'est l'interpréteur
|
||||
de l'image `apache/airflow:3.3.2-python3.12` retenue, et les tests d'intégrité doivent tourner sur
|
||||
@@ -121,35 +94,52 @@ environnement.
|
||||
|
||||
## Déploiement
|
||||
|
||||
`deploy.yml` est le huitième workflow (`backend`, `frontend`, `ml`, `infra`, `airflow`,
|
||||
`sonarqube`, `dast`, plus lui-même), et le seul qui ne tourne pas chez GitHub : il s'exécute sur
|
||||
un runner auto-hébergé installé sur la VM ENI, label `eni-g3`, parce que les runners hébergés ne
|
||||
joignent pas une adresse privée d'école. Le runner se connecte en sortie vers GitHub, aucun port
|
||||
entrant n'est ouvert.
|
||||
`deploy.yml` est le seul workflow qui ne tourne pas chez GitHub : il s'exécute sur un runner
|
||||
auto-hébergé installé sur la VM ENI, label `eni-g3`, parce que les runners hébergés ne joignent
|
||||
pas une adresse privée d'école. Le runner se connecte en sortie vers GitHub, aucun port entrant
|
||||
n'est ouvert. Il n'a pas de déclencheur propre en dehors de `workflow_dispatch` : c'est le job
|
||||
`deploy` de `ci.yml` qui l'appelle, sur un push, une fois « CI ok » vert.
|
||||
|
||||
| Événement | Environnement GitHub | Dossier sur la VM | Garde |
|
||||
|---|---|---|---|
|
||||
| `push` sur `dev` | `rec` | `/srv/enervision/rec` | aucune : la recette suit `dev` |
|
||||
| `push` sur `main` | `prod` | `/srv/enervision/prod` | approbation d'un relecteur dans l'environnement `prod`, branche `main` seule autorisée |
|
||||
| `push` sur `dev`, « CI ok » vert | `rec` | `/srv/enervision/rec` | aucune de plus : la recette suit `dev` |
|
||||
| `push` sur `main`, « CI ok » vert | `prod` | `/srv/enervision/prod` | approbation d'un relecteur dans l'environnement `prod`, branche `main` seule autorisée |
|
||||
| `workflow_dispatch` sur toute autre branche | `dev` | `/srv/enervision/dev` | droit d'écriture sur le dépôt, seul à pouvoir lancer un workflow ([ADR 0017](../adr/0017-environnement-dev-a-la-demande.md)) |
|
||||
|
||||
Le job aligne le clone sur la branche (`fetch`, `checkout`, `reset --hard`), lance
|
||||
Le job aligne le clone sur **le commit testé** (`fetch`, `checkout`, `reset --hard $GITHUB_SHA`),
|
||||
et non sur la pointe de branche du moment, qui a pu avancer pendant la CI. Il lance
|
||||
`make stack-up`, qui reconstruit les images, redémarre les conteneurs puis applique les
|
||||
migrations Alembic dans le conteneur backend, et attend jusqu'à trois minutes que
|
||||
`/api/v1/health/ready` réponde derrière le proxy. Cette sonde ne vérifie que la connexion à la
|
||||
base et la présence de TimescaleDB : sans la migration, le déploiement serait vert sur une base
|
||||
sans schéma, et c'est pourquoi `make stack-up` la porte. Un groupe de concurrence par branche,
|
||||
sans annulation, empêche deux déploiements simultanés du même environnement.
|
||||
sans schéma, et c'est pourquoi `make stack-up` la porte.
|
||||
|
||||
Les CI de deux push rapprochés peuvent finir dans le désordre. Deux gardes empêchent un
|
||||
environnement de reculer ou de sauter un commit :
|
||||
|
||||
- un commit qui **précède** celui déjà déployé depuis la même branche est ignoré, avec une
|
||||
annotation dans le run. Dans `dev`, une autre branche que celle en place est toujours déployée ;
|
||||
- les déploiements d'un même environnement passent un par un sous un verrou `flock` posé dans le
|
||||
clone de la VM, y compris deux branches lancées coup sur coup dans `dev`. Un groupe
|
||||
`concurrency` ne convenait pas : GitHub n'y garde qu'un job en attente, et un troisième arrivé
|
||||
l'annule sans erreur.
|
||||
|
||||
Le job ne fait pas de `actions/checkout` dans son espace de travail, et c'est voulu : le dossier
|
||||
de l'environnement est stable, hors du runner, parce que `.env`, certificats et volumes doivent
|
||||
survivre d'un déploiement à l'autre.
|
||||
|
||||
**Piège à connaître.** Un runner auto-hébergé sur un dépôt public exécute ce qu'un workflow lui
|
||||
envoie, et une PR de fork peut réécrire un workflow. Trois parades, et les trois sont
|
||||
nécessaires : `deploy.yml` ne se déclenche jamais sur `pull_request` ; le runner tourne sous un
|
||||
utilisateur dédié membre du groupe `docker`, jamais root ; le dépôt doit exiger une approbation
|
||||
pour les workflows des PR externes (Settings, Actions, « Require approval for all outside
|
||||
collaborators »), ce qui reste à activer. Les workflows de CI restent sur `ubuntu-latest`.
|
||||
envoie, et une PR de fork peut ajouter son propre workflow qui vise le label `eni-g3`. L'absence
|
||||
de `pull_request` dans `deploy.yml` ne suffit donc pas. Ce qui protège vraiment le runner :
|
||||
|
||||
- le dépôt exige l'approbation des workflows de tous les contributeurs externes (Settings,
|
||||
Actions, « Require approval for all external contributors ») ;
|
||||
- les environnements `rec` et `prod` n'acceptent que leur branche (`dev`, `main` avec un
|
||||
relecteur), ce qui bloque un job qui les déclare avant qu'il atteigne le runner ;
|
||||
- le runner tourne sous un utilisateur dédié membre du groupe `docker`, jamais root.
|
||||
|
||||
Les deux réglages de dépôt restent à activer par l'administratrice. Tous les autres workflows
|
||||
restent sur `ubuntu-latest`.
|
||||
|
||||
Cet utilisateur dédié doit posséder `/srv/enervision` : sinon git refuse les deux clones pour
|
||||
propriété douteuse et le `.env` en `600` lui échappe. `PROPRIETAIRE=<utilisateur du runner>`
|
||||
@@ -157,7 +147,7 @@ passé à `scripts/provision-host.sh` fixe ce propriétaire.
|
||||
|
||||
La machine se prépare avec `scripts/provision-host.sh`, qui vérifie Docker et Compose 2.24.4 ou
|
||||
plus, clone les deux branches, génère les secrets de chaque `.env` et les certificats
|
||||
auto-signés, et ne démarre rien. Le détail des deux environnements, ports et noms d'hôte, est
|
||||
auto-signés, et ne démarre rien. Le détail des trois environnements, ports et noms d'hôte, est
|
||||
dans [10-infra.md](10-infra.md).
|
||||
|
||||
## Ce qui bloque un merge
|
||||
@@ -179,6 +169,14 @@ dans [10-infra.md](10-infra.md).
|
||||
| Intégrité des DAGs | airflow | chargement des DAGs sans erreur d'import | Bloque |
|
||||
| Construction de l'image Airflow | airflow | `docker build` de `etl/airflow/Dockerfile` | Bloque |
|
||||
| Formatage et validité Terraform | infra | `fmt -check -recursive`, puis `init` et `validate` par racine | Bloque |
|
||||
| Verrous uv à jour | backend, ml, airflow | `uv sync --locked` : un `uv.lock` qui ne suit plus `pyproject.toml` échoue | Bloque |
|
||||
| Fichiers Compose | infra | `docker compose config` sur la stack de dev et la stack déployée, tous profils | Bloque |
|
||||
| Supervision | infra | `promtool check config`, `promtool test rules` (un cas par alerte), `amtool check-config`, JSON des tableaux | Bloque |
|
||||
| Workflows | infra | `actionlint`, shellcheck compris sur les blocs `run:` | Bloque |
|
||||
| Parcours de bout en bout | e2e | 18 parcours Playwright contre la stack de prod (proxy TLS) | Bloque |
|
||||
| Tir k6 de fumée | e2e | p95 < 500 ms et p99 < 1 s sur les lectures, moins de 1 % d'échecs | Bloque |
|
||||
| Limitation de débit | e2e | k6 par le proxy : des 429 au-delà de 20 req/s, aucune 5xx | Bloque |
|
||||
| **CI ok** | ci.yml | aucun job en `failure` ou `cancelled` | Bloque, **seul check à exiger** |
|
||||
|
||||
Deux seuils portent une décision qu'il faut savoir défendre :
|
||||
|
||||
@@ -218,7 +216,7 @@ qu'évite déjà le choix de l'image `timescaledb-ha` plutôt qu'un `postgres` n
|
||||
donc les deux environnements uv, applique `alembic upgrade head`, puis joue `-m integration` côté
|
||||
`ml/` et `-m chaine` côté backend.
|
||||
|
||||
Conséquence sur le déclenchement : les `paths` de `ml.yml` incluent `apps/backend/alembic/**` et
|
||||
Conséquence sur le déclenchement : le filtre `ml` de `ci.yml` inclut `apps/backend/alembic/**` et
|
||||
`apps/backend/app/models/**`. Sans eux, une migration qui renomme une colonne de `reading` ne
|
||||
déclencherait pas ce job, le SQL brut du pipeline dériverait du schéma, et **rien ne casserait
|
||||
avant la production**. Le prix est qu'une PR touchant seulement une migration lance aussi le lint
|
||||
@@ -233,10 +231,18 @@ poste où `ml/` n'est pas installé.
|
||||
|
||||
## SonarCloud, et l'incident qui a immobilisé trois PR
|
||||
|
||||
Le workflow `sonarqube.yml` exécute cinq jobs de préparation (`build-front`, `test-front`,
|
||||
`build-back`, `test-back`, `test-ml`) dont les tests produisent chacun un rapport de couverture en
|
||||
artefact, puis un dernier job qui les télécharge et lance `SonarSource/sonarqube-scan-action@v8`
|
||||
avec le secret `SONAR_TOKEN`. Le périmètre est décrit par `sonar-project.properties` à la racine.
|
||||
Le job `sonar` de `ci.yml` ne reconstruit ni ne reteste rien. Les jobs `verification` de
|
||||
`backend.yml`, `ml.yml` et `frontend.yml` versent leur rapport de couverture en artefact, et
|
||||
`sonar` les télécharge dans le même run, un par un (backend et ML nomment tous deux le leur
|
||||
`coverage.xml`), puis lance `SonarSource/sonarqube-scan-action`, épinglée sur un SHA, avec le
|
||||
secret `SONAR_TOKEN`. Il ne tourne ni pour Dependabot ni pour une PR de fork, qui n'ont pas ce
|
||||
secret. Le périmètre est décrit par `sonar-project.properties` à la racine, seul fichier de
|
||||
configuration Sonar du dépôt.
|
||||
|
||||
Jusqu'au 23/09, un `sonarqube.yml` à part rejouait build et tests des trois modules pour produire
|
||||
ces rapports, en double exact des workflows qui le faisaient déjà. Les exclusions de
|
||||
`sonar-project.properties` sont aussi passées en globs (`**/tests/**`, `**/alembic/**`) : un
|
||||
motif sans `**` ne vise que la racine du dépôt.
|
||||
|
||||
Le périmètre couvre `apps/frontend`, `apps/backend`, `ml/` et `etl/airflow` (les deux derniers
|
||||
ajoutés après coup : ils n'étaient pas analysés, une PR qui ne touchait qu'eux ne lançait pas
|
||||
@@ -261,9 +267,10 @@ contournée** en désactivant la gate ou en excluant les fichiers gênants.
|
||||
|
||||
## Dependabot
|
||||
|
||||
`.github/dependabot.yml` déclare **six entrées hebdomadaires groupées, sur cinq écosystèmes** :
|
||||
`npm` sur `/apps/frontend`, `uv` sur `/apps/backend`, `github-actions` sur `/`, `docker` sur les
|
||||
deux dossiers d'application, et `docker-compose` sur `/`. Les mises à jour arrivent en PR, donc
|
||||
`.github/dependabot.yml` déclare **sept entrées hebdomadaires, sur cinq écosystèmes** : `npm`
|
||||
sur `/apps/frontend` et sur `/tests/e2e`, `uv` sur `/apps/backend`, `github-actions` sur `/`,
|
||||
`docker` sur les deux dossiers d'application, et `docker-compose` sur `/`, qui suit aussi les
|
||||
images de supervision et de k6. Les mises à jour arrivent en PR, donc
|
||||
elles traversent les mêmes gates que n'importe quel changement : une montée de version qui casse
|
||||
les tests ne se merge pas.
|
||||
|
||||
@@ -300,10 +307,10 @@ UTC, et sur une PR qui modifie le scan lui-même. Pas à chaque PR : un scan act
|
||||
minutes.
|
||||
|
||||
Le job démarre sur le runner la base (même image TimescaleDB que `docker-compose.yml`, base
|
||||
jetable), applique les migrations, y sème un site et deux relevés (`db/seeds/` est vide, pas
|
||||
encore d'outillage de jeu de données pour la CI ; sans données, `GET /sites` rend `[]`, chaque
|
||||
`/{site_id}` rend 404, et le scan actif ne frappe que des gestionnaires d'erreur), démarre le
|
||||
backend, puis `scripts/dast-token.sh` crée un compte **`lecteur`** et rend son jeton.
|
||||
jetable), applique les migrations, y sème `db/seeds/demo.sql` (sans données, `GET /sites` rend
|
||||
`[]`, chaque `/{site_id}` rend 404, et le scan actif ne frappe que des gestionnaires d'erreur),
|
||||
démarre le backend, puis `scripts/dast-token.sh` s'appuie sur `scripts/comptes-test.sh` pour
|
||||
créer les comptes et rend le jeton du **`lecteur`**. Le jeu et les comptes sont ceux de l'e2e.
|
||||
|
||||
ZAP charge le contrat `/openapi.json` depuis un fichier (`zap-api-scan.py -f openapi -t
|
||||
/zap/wrk/openapi.json`) et en importe les 26 opérations **quel que soit le jeton** : c'est le
|
||||
@@ -392,15 +399,34 @@ pas de TLS, pas de reverse proxy). Il remontera des alertes qui n'existent pas d
|
||||
(HSTS absent...) et ne dit **rien** des en-têtes ni du TLS que le proxy pose en production. Un
|
||||
second passage sur la stack complète reste à faire.
|
||||
|
||||
## Tests de bout en bout et de charge
|
||||
|
||||
Le workflow `e2e.yml` démarre la stack telle qu'elle est déployée, derrière le proxy TLS, sur
|
||||
`https://localhost` ([ADR 0015](../adr/0015-tests-e2e-et-de-charge-contre-la-stack-compose.md)) :
|
||||
|
||||
1. Il construit et démarre `db`, `mailpit`, `backend`, `frontend` et `proxy` avec
|
||||
`docker-compose.prod.yml`, sans Airflow. C'est le seul job qui construit les images backend et
|
||||
frontend avant un déploiement.
|
||||
2. Il migre la base, pose le rôle `supervision`, sème `db/seeds/demo.sql` et crée les comptes
|
||||
(`scripts/comptes-test.sh`).
|
||||
3. Il joue les 18 parcours Playwright de `tests/e2e`, sur un seul worker et avec une session par
|
||||
fichier. Le rapport HTML et les traces du premier réessai sont versés en artefact.
|
||||
4. Il lance le tir k6 `smoke`, directement sur `backend:8000`, puis `limitation-debit` par le
|
||||
proxy. Les synthèses s'affichent dans le résumé du job, et les rapports HTML sont versés en
|
||||
artefact.
|
||||
|
||||
La charge nominale (`make load-test`) et le stress (`make load-stress`) ne tournent pas en CI :
|
||||
voir `tests/load/README.md`.
|
||||
|
||||
## Ce qui manque, et pourquoi
|
||||
|
||||
| Manque | Issue | Conséquence assumée |
|
||||
|---|---|---|
|
||||
| Images publiées et promues par digest (GHCR) | aucune | Chaque environnement reconstruit ses images : la production n'exécute pas l'artefact validé en recette, mais un second build du même commit |
|
||||
| DAST bloquant | #41 | Le scan ZAP existe mais ne bloque rien : aucun seuil n'est fixé tant que les alertes du premier passage ne sont pas triées |
|
||||
| Tests end to end | #46 | Les parcours utilisateur ne sont pas vérifiés en CI |
|
||||
| Tests de charge | #47 | Aucun garde-fou de performance |
|
||||
| Scan d'image de conteneur | aucune | Les `Dockerfile` sont construits en local, pas analysés |
|
||||
| Tir de charge nominal automatisé | #47 | Seul le smoke tourne en CI ; la charge à 50 utilisateurs se lance à la main en recette (`make load-test`), rec et prod partageant la VM |
|
||||
| Cache de couches Docker en CI | aucune | Le job E2E reconstruit les images backend et frontend à chaque run, deux à quatre minutes de plus |
|
||||
| Scan d'image de conteneur | aucune | Les images sont construites par le job E2E, pas analysées |
|
||||
|
||||
## Reproduire la CI en local
|
||||
|
||||
@@ -417,6 +443,14 @@ make ml-test-integration # pipeline ML, marqueur `integration`
|
||||
make test-chaine # vrais binaires ML puis relecture par l'API, marqueur `chaine`
|
||||
```
|
||||
|
||||
Les autres jobs se rejouent aussi sur le poste :
|
||||
|
||||
```bash
|
||||
make e2e-prepare e2e # parcours Playwright contre `make dev` (tests/e2e/README.md)
|
||||
make load-smoke K6_EMAIL=... K6_PASSWORD=... # tir k6 d'une minute (tests/load/README.md)
|
||||
make monitoring-check # promtool, amtool et JSON des tableaux de bord, comme le job Infra
|
||||
```
|
||||
|
||||
Le SAST se rejoue à l'identique : `uvx bandit==1.9.4 --recursive app --severity-level medium
|
||||
--confidence-level medium` depuis `apps/backend`, et la même commande sur `enervision_ml` depuis
|
||||
`ml`.
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# Observabilité
|
||||
|
||||
Ce document décrit ce qu'on voit du système en fonctionnement : métriques, tableaux de bord,
|
||||
alertes et journaux. Décision dans l'[ADR 0016](../adr/0016-supervision-en-profil-compose.md),
|
||||
mode d'emploi dans [`monitoring/README.md`](../../monitoring/README.md).
|
||||
|
||||
| Brique | Sert à | Statut |
|
||||
|---|---|---|
|
||||
| Métriques de l'API | Débit, erreurs et latences par route | `Fait` |
|
||||
| Collecte et alertes | Prometheus, neuf règles testées, Alertmanager vers Mailpit | `Fait` |
|
||||
| Tableaux de bord | Grafana : API, données et modèle, infrastructure | `Fait` |
|
||||
| Métriques d'Airflow | StatsD ou OpenTelemetry des DAGs | `Cible` |
|
||||
| Journaux centralisés | Loki ou équivalent | `Cible` |
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph projet["Projet Compose de la prod"]
|
||||
api["backend<br/>/metrics"]
|
||||
db[("db<br/>TimescaleDB")]
|
||||
mail["mailpit"]
|
||||
|
||||
subgraph sup["Profil monitoring"]
|
||||
prom["prometheus<br/>15 s, 15 jours"]
|
||||
am["alertmanager"]
|
||||
graf["grafana"]
|
||||
pge["postgres-exporter"]
|
||||
node["node-exporter"]
|
||||
cad["cadvisor"]
|
||||
end
|
||||
end
|
||||
|
||||
hote["Hôte : VM ENI<br/>recette et prod"]
|
||||
|
||||
prom -->|"Bearer APP_METRICS_TOKEN"| api
|
||||
prom --> pge & node & cad
|
||||
pge -->|"rôle supervision"| db
|
||||
node -.->|"/proc, /sys"| hote
|
||||
cad -.->|"cgroups"| hote
|
||||
prom -->|"règles franchies"| am -->|"SMTP"| mail
|
||||
graf --> prom
|
||||
graf -->|"rôle supervision, SQL"| db
|
||||
```
|
||||
|
||||
Tout vit dans le projet Compose de la prod, sur son réseau. La recette n'a pas de supervision
|
||||
propre. node-exporter et cAdvisor voient pourtant tout l'hôte : la mémoire de la VM et de chaque
|
||||
conteneur couvre donc aussi la recette, qu'on distingue au préfixe `enervision-rec-`.
|
||||
|
||||
## Ce que mesure chaque source
|
||||
|
||||
| Source | Métriques utiles | Où les lire |
|
||||
|---|---|---|
|
||||
| API (`prometheus-fastapi-instrumentator`) | `http_requests_total` par route et classe de statut, `http_request_duration_seconds` par route (seaux 50 ms à 2,5 s), `http_request_duration_highr_seconds` global, mémoire du processus | Tableau « API » |
|
||||
| postgres-exporter | `pg_up`, connexions par état, `max_connections`, transactions validées, taille des bases | Tableau « Infrastructure » |
|
||||
| node-exporter | Processeur, mémoire disponible, espace disque de `/` | Tableau « Infrastructure » |
|
||||
| cAdvisor | Mémoire (`working_set`) et processeur par conteneur | Tableau « Infrastructure » |
|
||||
| TimescaleDB, en SQL | Fraîcheur des relevés par site, relevés ingérés par heure, alertes par sévérité, `drift_report` | Tableau « Données et modèle » |
|
||||
|
||||
Deux choix de l'instrumentation se lisent dans ces courbes :
|
||||
|
||||
- **Les sondes `/health/*` et `/metrics` ne sont pas comptées.** La sonde Docker frappe toutes
|
||||
les 30 s : incluse, elle ferait baisser la latence moyenne et gonfler le débit d'une API au
|
||||
repos.
|
||||
- **Les seaux par route encadrent 500 ms**, seuil de charge de l'ADR 0015. Grafana lit ainsi le
|
||||
même p95 que k6 pendant un tir.
|
||||
|
||||
## Alertes
|
||||
|
||||
| Groupe | Alertes | Sévérité |
|
||||
|---|---|---|
|
||||
| API | Indisponible 2 min, 5xx au-delà de 5 %, p95 au-delà d'une seconde | critical, critical, warning |
|
||||
| Base | PostgreSQL injoignable 2 min, connexions au-delà de 80 % | critical, warning |
|
||||
| Hôte | Mémoire au-delà de 90 %, disque sous 10 %, processeur au-delà de 90 % | warning, critical, warning |
|
||||
| Supervision | Un exporteur muet 5 min | warning |
|
||||
|
||||
- **Tests des règles.** Chaque règle a un cas dans `monitoring/prometheus/tests/`, joué par
|
||||
`promtool test rules` dans le job Infra de la CI. Une règle qui ne se déclenche plus, ou se
|
||||
déclenche à tort, casse la CI avant d'atteindre la prod.
|
||||
- **Envoi.** Alertmanager groupe les alertes par nom et sévérité et les envoie par courriel via
|
||||
Mailpit, qui les capture sans rien relayer. Un `critical` est rappelé toutes les heures, un
|
||||
`warning` toutes les douze. Un `critical` masque le `warning` de la même cible.
|
||||
|
||||
## Sécurité
|
||||
|
||||
- **Aucune interface exposée.** Prometheus, Alertmanager et Grafana n'écoutent que sur
|
||||
`127.0.0.1`, et rien ne passe par le proxy (ADR 0007). Accès par tunnel SSH.
|
||||
- **`/metrics` gardé par jeton.** Il n'est pas routé par nginx, et Prometheus y présente
|
||||
`APP_METRICS_TOKEN`, que l'API exige dès qu'il est posé. Le jeton lui parvient en secret
|
||||
Compose, jamais en clair dans sa configuration.
|
||||
- **Base en lecture seule.** Grafana et postgres-exporter lisent la base par le rôle
|
||||
`supervision`, en lecture seule, limité aux tables métier (`db/roles/supervision.sql`). Ils
|
||||
n'ont ni `app_user`, ni jetons, ni journal d'audit.
|
||||
- **Grafana verrouillé.** Il refuse de démarrer sans `GRAFANA_ADMIN_PASSWORD`. Inscription,
|
||||
accès anonyme et appels sortants (statistiques d'usage, vérification de mises à jour) y sont
|
||||
désactivés.
|
||||
- **cAdvisor en `privileged`.** Il tourne ainsi pour lire les cgroups, avec des montages en
|
||||
lecture seule et sans port publié.
|
||||
|
||||
## Journaux
|
||||
|
||||
Les journaux restent ceux de Docker : `docker compose logs`, `make stack-logs`,
|
||||
`make monitoring-logs`. L'API écrit du JSON dès `APP_ENV=prod`, caviardé des jetons et des mots
|
||||
de passe (voir [20-backend.md](20-backend.md)). Aucune agrégation centralisée n'est en place.
|
||||
|
||||
## Ce qui manque
|
||||
|
||||
| Manque | Conséquence assumée |
|
||||
|---|---|
|
||||
| Métriques d'Airflow (StatsD, OpenTelemetry) | Un DAG qui échoue ne se voit que dans Airflow ; le tableau « Données » le trahit indirectement par des relevés qui vieillissent |
|
||||
| Alerte sur la fraîcheur des relevés | Visible dans Grafana, mais aucune règle Prometheus ne la porte : il faudrait une métrique calculée par l'API ou un exportateur SQL |
|
||||
| Journaux centralisés | Un incident se diagnostique conteneur par conteneur |
|
||||
| Destinataire réel des alertes | Mailpit capture tout : les alertes se lisent dans son interface, elles ne réveillent personne |
|
||||
@@ -15,12 +15,12 @@ contredisent, c'est l'ADR qui fait foi et la vue qui est en retard.
|
||||
| [31-contrat-authentification.md](31-contrat-authentification.md) | Ce que le frontend doit savoir pour coder la connexion |
|
||||
| [32-design-systeme-frontend.md](32-design-systeme-frontend.md) | Tokens CSS, composants `ev-*` partagés, règle anti-couleur-en-dur |
|
||||
| [40-data.md](40-data.md) | Frontières `db/` et `alembic/`, cycle de vie d'une mesure, modèle |
|
||||
| [50-cicd.md](50-cicd.md) | Workflows, gates bloquantes, SonarCloud, Dependabot, ce qui manque |
|
||||
| [50-cicd.md](50-cicd.md) | Orchestrateur `ci.yml`, gates bloquantes, e2e et charge, SonarCloud, Dependabot, ce qui manque |
|
||||
| [60-observabilite.md](60-observabilite.md) | Métriques, Prometheus, alertes, tableaux de bord Grafana, ce qui manque |
|
||||
|
||||
La CI/CD a désormais son document : cinq workflows et seize jobs, c'est assez de matière pour
|
||||
qu'une section de plus dans une autre vue devienne illisible. L'observabilité, elle, n'en a
|
||||
toujours pas : `monitoring/` ne contient que des `.gitkeep`. Elle en sortira le jour où elle aura
|
||||
de la matière. Un fichier vide de plus n'aide personne.
|
||||
La CI/CD a son document : un orchestrateur et ses workflows de composant, c'est assez de
|
||||
matière pour qu'une section de plus dans une autre vue devienne illisible. L'observabilité a le
|
||||
sien depuis l'issue #26, qui lui a donné de la matière : collecte, alertes et tableaux de bord.
|
||||
|
||||
L'orchestration Airflow, elle, en a depuis les issues #115 et #116 : trois DAGs, leur image et
|
||||
leurs contraintes sont décrits dans [10-infra.md](10-infra.md).
|
||||
|
||||
Reference in New Issue
Block a user