Merge remote-tracking branch 'origin/dev' into docs/livrables-ec03-ec06
# Conflicts: # docs/architecture/00-vue-ensemble.md
This commit is contained in:
@@ -57,7 +57,7 @@ flowchart TB
|
||||
navigateur --> front
|
||||
front -.-> api
|
||||
api --> db
|
||||
airflow -.-> db
|
||||
airflow --> db
|
||||
prom -.-> api
|
||||
grafana -.-> db
|
||||
grafana -.-> prom
|
||||
@@ -67,6 +67,10 @@ Le lien `front -.-> api` reste en pointillé : le frontend appelle bien une API,
|
||||
intercepteur répond à sa place tant que les endpoints n'existent pas. Voir
|
||||
[30-frontend.md](30-frontend.md).
|
||||
|
||||
Le lien `airflow --> db` est maintenant en trait plein : deux DAGs orchestrent l'entraînement et
|
||||
le scoring du modèle ML (issue #115), cf. plus bas et [20-backend.md](20-backend.md). Le reste du
|
||||
périmètre Airflow envisagé (ingestion, issues #15/#16) reste en pointillé, non construit.
|
||||
|
||||
Le lien `prom -.-> api` de même : l'API expose bien `/metrics` au format Prometheus, mais aucun
|
||||
collecteur ne vient le lire.
|
||||
|
||||
@@ -80,14 +84,15 @@ collecteur ne vient le lire.
|
||||
| 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`. Voir [ADR 0005](../adr/0005-modele-prediction-lightgbm.md) et [ML-START.md](../ML-START.md). Automatisation (Airflow) et surveillance de dérive (EC06, #44/#45) pas encore construites |
|
||||
| Infra | Terraform, k3s single-node | `infra/terraform` | `En cours` | Module d'installation du cluster. Jamais appliqué, aucune ressource Kubernetes déclarée |
|
||||
| Monitoring | Prometheus, Grafana, Alertmanager | `monitoring` | `Cible` | Rien, hors le `/metrics` exposé par l'API |
|
||||
| ETL | Apache Airflow | `etl/airflow` | `Cible` | Rien |
|
||||
| CI/CD | GitHub Actions | `.github/workflows` | `En cours` | 4 workflows, 14 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. Détail dans [50-cicd.md](50-cicd.md). **Aucun job de déploiement** (#21) |
|
||||
| ETL | Apache Airflow | `etl/airflow` | `En cours` | Webserver + scheduler (LocalExecutor) tournent via docker-compose, base de métadonnées Postgres dédiée. Deux DAGs (`ml_train` manuel, `ml_score` `@hourly`) orchestrent le pipeline ML existant en sous-processus `uv run` (issue #115). L'ingestion (issues #15/#16) n'a pas encore de DAG |
|
||||
| CI/CD | GitHub Actions | `.github/workflows` | `En cours` | 5 workflows, 16 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. Détail dans [50-cicd.md](50-cicd.md). **Aucun job de déploiement** (#21) |
|
||||
|
||||
## Flux bout en bout
|
||||
|
||||
Statut : `En cours`. Tout le chemin de lecture existe (base, API, frontend), ainsi que l'ingestion
|
||||
par import depuis un CSV historique et depuis l'API Mock. **Le seul maillon absent est
|
||||
l'orchestration** : Airflow ne tourne pas, l'ingestion et le scoring sont lancés à la main.
|
||||
Statut : `En cours`. **Le chemin de lecture tourne** : base, API et frontend. **Le chemin
|
||||
d'ingestion dessiné ci-dessous n'existe pas** : les deux DAGs livrés (`ml_train`, `ml_score`,
|
||||
issue #115) orchestrent le pipeline ML, pas l'ingestion, qui reste lancée à la main par les
|
||||
scripts d'import (issues #15 et #16).
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
|
||||
@@ -40,13 +40,63 @@ seule la base tourne en conteneur, l'API et `ng serve` tournent sur le poste ave
|
||||
des deux seul). Le service `backend` sert la stack complète et la recette. Les deux occupent le
|
||||
port 8000, ils ne se lancent donc pas ensemble.
|
||||
|
||||
Deux pièges sont documentés en tête du `docker-compose.yml`, ils ne se devinent pas :
|
||||
Trois pièges sont documentés en tête du `docker-compose.yml`, ils ne se devinent pas :
|
||||
|
||||
- `PGDATA` vaut `/home/postgres/pgdata/data` pour l'image `-ha`, et non le chemin habituel de
|
||||
l'image `postgres`. Monté ailleurs, le volume ne retient rien, sans le moindre message.
|
||||
- `db/init` est monté **fichier par fichier**. Monter le dossier masquerait les scripts d'init de
|
||||
l'image, dont `timescaledb-tune`. Ajouter un fichier dans `db/init/` impose donc une ligne dans
|
||||
le compose. Voir [`db/README.md`](../../db/README.md).
|
||||
- `LocalExecutor` exécute les tâches comme sous-processus du **scheduler**, jamais du webserver :
|
||||
c'est le scheduler qui a besoin du volume `airflow_ml_state` (modèle, magasin MLflow).
|
||||
|
||||
### Airflow (`ml_train`/`ml_score`, issue #115)
|
||||
|
||||
Trois services, `docker compose profiles` non utilisés (démarrage explicite via `make
|
||||
airflow-up`, pas dans `make dev`) :
|
||||
|
||||
| Service | Rôle | Points notables |
|
||||
|---|---|---|
|
||||
| `airflow-init` | Migre la base de métadonnées, crée le compte admin | Conteneur jetable (`restart: "no"`), ne redémarre jamais. `webserver`/`scheduler` attendent qu'il se termine avec succès |
|
||||
| `airflow-webserver` | UI, port `8080` | `LocalExecutor` : n'exécute aucune tâche lui-même |
|
||||
| `airflow-scheduler` | Planifie et **exécute** les tâches (`LocalExecutor`) | Les DAGs y tournent en sous-processus (`uv run --frozen --no-dev python -m enervision_ml...`), c'est lui qui a besoin du volume `airflow_ml_state` |
|
||||
|
||||
Construits depuis `etl/airflow/Dockerfile`, contexte `.` (racine du repo, pas `etl/airflow/`) :
|
||||
l'image doit pouvoir `COPY` `ml/pyproject.toml`/`ml/uv.lock`/`ml/enervision_ml` pour se
|
||||
synchroniser un second environnement Python **3.14** (`/opt/ml/.venv`, `uv sync --locked` à la
|
||||
construction), distinct du Python 3.12 qui fait tourner Airflow lui-même. Les DAGs shellent vers
|
||||
ce venv plutôt que d'importer LightGBM/MLflow dans le process Airflow.
|
||||
|
||||
`airflow-init` s'appuie sur l'entrypoint de l'image (`_AIRFLOW_DB_MIGRATE`,
|
||||
`_AIRFLOW_WWW_USER_*`) plutôt que sur un script maison : l'entrypoint porte le code de sortie, une
|
||||
migration ratée (typiquement la base `airflow` absente, cf. ci-dessous) fait échouer le service et
|
||||
`webserver`/`scheduler` ne démarrent pas sur une base non migrée. Le mot de passe du compte admin
|
||||
passe par l'environnement, jamais par `argv` (ni `ps`, ni `docker compose config`).
|
||||
|
||||
Les variables `AIRFLOW_*` ne sont volontairement pas en `${VAR:?}` : Compose interpole le fichier
|
||||
entier avant de filtrer les services, une variable requise manquante casserait `make db-up`,
|
||||
`make dev`... pour tout poste dont le `.env` est antérieur. Elles valent `${VAR:-}` et c'est
|
||||
`airflow-init` qui refuse de démarrer (clé Fernet, clé Flask ou mot de passe vides).
|
||||
|
||||
**Pourquoi `ml_train` est manuel.** Réentraîner est coûteux et sa cadence n'est pas une décision
|
||||
prise. Surtout, `train.py` écrase le modèle sans comparer ses métriques à celles de l'ancien : un
|
||||
cron déploierait silencieusement un modèle dégradé. Tant que ce garde-fou n'existe pas, le
|
||||
déclenchement reste humain. `ml_score`, lui, est planifié à l'heure, avec `max_active_runs=1`
|
||||
(pas deux scorings simultanés dans `prediction`), 2 tentatives et un plafond de 30 minutes.
|
||||
|
||||
CI : `.github/workflows/airflow.yml` (Python 3.12 via `etl/airflow/.python-version`) lance lint et
|
||||
tests d'intégrité des DAGs, et construit l'image (elle `COPY` `ml/`, une modification de `ml/`
|
||||
peut donc la casser) avant de vérifier que le pipeline s'y importe sans réseau.
|
||||
|
||||
Piège à connaître : sur un volume `pgdata` déjà peuplé (poste de dev existant plutôt que premier
|
||||
`make db-up`), `db/init/120-airflow-database.sql` ne se rejoue pas (PostgreSQL n'exécute
|
||||
`docker-entrypoint-initdb.d/` que sur un volume vide). Créer la base `airflow` à la main une fois :
|
||||
`docker compose exec db psql -U $POSTGRES_USER -d $POSTGRES_DB -c "CREATE DATABASE airflow;"`.
|
||||
|
||||
`libgomp1` est installé explicitement dans l'image (`apt-get`, en root) : l'image Airflow de base
|
||||
est minimale et n'embarque pas la runtime OpenMP dont LightGBM a besoin, sans quoi l'erreur
|
||||
(`OSError: libgomp.so.1`) n'apparaît qu'à la première tâche réellement exécutée, pas à la
|
||||
construction de l'image.
|
||||
|
||||
## Cible de déploiement
|
||||
|
||||
@@ -116,6 +166,8 @@ Ces arbitrages sont pris. Ils ne vivaient jusqu'ici que dans des commentaires de
|
||||
| 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` |
|
||||
| Webserver Airflow | `8080` | `make airflow-up`. Scheduler et webserver ne publient que ce port ; les tâches (`LocalExecutor`) tournent côté scheduler, sans port propre |
|
||||
|
||||
## Le trou entre les deux topologies
|
||||
|
||||
|
||||
@@ -256,8 +256,8 @@ auraient pu comparer des lectures/choisir une prévision au hasard. `_detect_spi
|
||||
explicitement les paires de lectures qui partagent le même horodatage (deux `source` pour un seul
|
||||
instant réel, pas une variation).
|
||||
|
||||
Comme `enervision_ml.score`, la détection est un script lancé à la main, pas encore ordonnancé par
|
||||
Airflow : `uv run python -m app.detection.internal_alerts [--site-id ...] [--now ...]`, dans
|
||||
La détection est un script lancé à la main, pas encore ordonnancé par Airflow (contrairement à
|
||||
`enervision_ml.score`, orchestré par le DAG `ml_score` depuis l'issue #115) : `uv run python -m app.detection.internal_alerts [--site-id ...] [--now ...]`, dans
|
||||
`apps/backend` puisque les règles s'appuient sur les repositories ORM de l'API plutôt que sur une
|
||||
connexion SQL directe (contrairement à `app/etl/historical_import.py`). Cette issue (#104)
|
||||
débloquait #38 (moteur de règles pour recommandations), dont la FK `alert_id` `NOT NULL` n'avait
|
||||
|
||||
@@ -36,6 +36,11 @@ flowchart TB
|
||||
ms["sast<br/>bandit"]
|
||||
end
|
||||
|
||||
subgraph afw["Airflow · airflow.yml"]
|
||||
av["verification<br/>ruff, intégrité des DAGs"]
|
||||
ab["image<br/>construction de l'image"]
|
||||
end
|
||||
|
||||
subgraph sq["SonarQube · sonarqube.yml"]
|
||||
sb1["build-front / test-front"]
|
||||
sb2["build-back / test-back"]
|
||||
@@ -46,24 +51,33 @@ flowchart TB
|
||||
push --> fb --> ft
|
||||
push --> fd
|
||||
push --> mv & ms
|
||||
push --> av & ab
|
||||
push --> sb1 & sb2 --> sscan
|
||||
sscan -.-> cd["deploy<br/>issue #21"]
|
||||
```
|
||||
|
||||
## Déclenchement
|
||||
|
||||
Les quatre workflows se déclenchent sur `push` **et** sur `pull_request`, filtrés par **chemin** :
|
||||
Les cinq workflows 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/**`,
|
||||
chacun incluant son propre fichier de workflow dans le filtre pour qu'une modification du pipeline
|
||||
déclenche le pipeline.
|
||||
`airflow.yml` sur `etl/airflow/**` **et sur `ml/**`**, chacun incluant son propre fichier de
|
||||
workflow dans le filtre pour qu'une modification du pipeline déclenche le pipeline.
|
||||
|
||||
Le filtre d'`airflow.yml` mérite un mot : il inclut `ml/pyproject.toml`, `ml/uv.lock` et
|
||||
`ml/enervision_ml/**` parce que l'image Airflow copie le code et les dépendances du module ML.
|
||||
Une modification de `ml/` peut donc casser la construction de cette image, et le filtre le voit.
|
||||
|
||||
**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.
|
||||
|
||||
`backend.yml` et `ml.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.
|
||||
`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.
|
||||
|
||||
**Piège de version** : `etl/airflow` tourne en **Python 3.12** et non 3.14, parce qu'Airflow 2.10
|
||||
ne supporte pas encore 3.14. Le 3.14 du module ML ne vit, dans ce contexte, que dans l'image
|
||||
Docker et son propre environnement.
|
||||
|
||||
## Ce qui bloque un merge
|
||||
|
||||
@@ -79,6 +93,8 @@ rien changer), mais ce serait à borner sur un dépôt à forte fréquence de pu
|
||||
| **SAST `bandit`** | backend (`app`), ml (`enervision_ml`) | **MEDIUM et au-dessus** | Bloque |
|
||||
| Quality gate SonarCloud | tout le dépôt | gate par défaut, couverture du **code neuf** | Bloque |
|
||||
| Build `npm run build` | frontend | compilation | Bloque |
|
||||
| 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 |
|
||||
|
||||
Deux seuils portent une décision qu'il faut savoir défendre :
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ contredisent, c'est l'ADR qui fait foi et la vue qui est en retard.
|
||||
| [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 |
|
||||
|
||||
La CI/CD a désormais son document : quatre workflows et quatorze jobs, c'est assez de matière pour
|
||||
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.
|
||||
|
||||
@@ -57,7 +57,7 @@ règles Bandit. Ajouter Bandit à la CI serait redondant, contrairement à ce qu
|
||||
| **API10 Unsafe Consumption of APIs** | **partiel, et spécifique à ce projet** | L'API Mock de l'école n'a aucune authentification, tourne en HTTP clair sur le réseau de l'école, et expose un endpoint mutatif à quiconque. Sa réponse est traitée comme une entrée hostile par `app/etl/mock_api_import.py`, son seul consommateur à ce jour : les quatre garde-fous attendus sont en place, voir la ligne correspondante plus haut. Reste ouvert : le plafond de taille s'applique après désérialisation de la réponse, borner le corps HTTP lui-même demanderait une lecture en flux ; et `APP_MOCK_API_BASE_URL` n'impose pas `https`, donc les identifiants Basic partiraient en clair sur une URL en `http`. La conséquence la plus sérieuse n'est pas la fausse alerte, c'est l'empoisonnement du jeu d'entraînement du modèle de prédiction. |
|
||||
| **A08 Software and Data Integrity Failures** | **partiel** | La CI vérifie le code mais n'analyse ni les dépendances ni les images. `.terraform.lock.hcl` reste ignoré par git, ce qui contredit une chaîne d'approvisionnement maîtrisée. |
|
||||
| **A10 Server-Side Request Forgery** | **sans objet aujourd'hui** | Aucune URL sortante n'est pilotée par une donnée utilisateur. Le jour où l'adresse d'une source devient un champ de configuration, il faudra une liste blanche de schémas et d'hôtes, sans suivi de redirection. |
|
||||
| **Cantonnement des accès ETL et ML** | **dette assumée** | Le compte applicatif porte l'identité, le rôle PostgreSQL porterait le cantonnement. Voir ADR 0003. |
|
||||
| **Cantonnement des accès ETL et ML** | **dette assumée** | Le compte applicatif porte l'identité, le rôle PostgreSQL porterait le cantonnement. Voir ADR 0003. Plus coûteuse depuis Airflow (#115) : ce service publie le port 8080, détient les identifiants Postgres complets (`ML_DATABASE_URL`, mêmes que le backend) et permet de déclencher l'exécution de code depuis son interface. Un compte Airflow compromis atteint donc toute la base, pas seulement `reading`/`site`. Le compte admin Airflow est distinct des `app_user` et son mot de passe passe par l'environnement, jamais par `argv`. |
|
||||
| **Non-répudiation de l'audit** | **dette assumée** | Les déclencheurs arrêtent les accidents, pas un compte détenant `ALTER TABLE`. Voir ADR 0004. |
|
||||
|
||||
## Ce qu'il faut répondre, et ne pas répondre
|
||||
|
||||
Reference in New Issue
Block a user