diff --git a/README.md b/README.md index 33426fb..ec5ffd9 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Ce que la documentation apporte à chacun : [docs/architecture/00-vue-ensemble.m | Backend | FastAPI, Python 3.14 | `apps/backend` | Initialise | | Frontend | Angular 22, Node 24 LTS | `apps/frontend` | Tableau de bord | | Base | PostgreSQL 17 + TimescaleDB | `db` | Initialise | -| ETL | Apache Airflow | `etl/airflow` | A initialiser | +| ETL | Apache Airflow | `etl/airflow` | Trois DAGs | | Infra | Terraform (k3s single-node) | `infra/terraform` | Initialise | | CI/CD | GitHub Actions | `.github/workflows` | Backend en place | | Monitoring | Prometheus, Grafana, Alertmanager | `monitoring` | A initialiser | @@ -47,7 +47,7 @@ L'etat detaille de chaque brique et les vues d'architecture sont dans │ ├── migrations/ Migrations SQL versionnees │ └── seeds/ Jeux de donnees de reference ├── etl/airflow/ -│ ├── dags/ DAGs d'ingestion et d'agregation +│ ├── dags/ DAGs d'orchestration (pipeline ML, alertes) │ ├── plugins/ Operateurs et hooks maison │ ├── include/ Requetes SQL et ressources des DAGs │ └── tests/ Tests d'integrite des DAGs diff --git a/docs/README.md b/docs/README.md index 17859f5..bbd6978 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,3 +11,6 @@ | [0002](adr/0002-authentification-jwt-et-refresh-opaque.md) | Authentification par JWT d'accès et jeton de rafraîchissement opaque | | [0003](adr/0003-autorisation-rbac-a-trois-roles.md) | Autorisation RBAC à trois rôles, relecture du compte à chaque requête | | [0004](adr/0004-journal-d-audit-en-ajout-seul.md) | Journal d'audit en ajout seul, garanti par PostgreSQL | +| [0005](adr/0005-modele-prediction-lightgbm.md) | LightGBM pour la prédiction de consommation, un modèle global | +| [0006](adr/0006-moteur-de-regles-dans-le-backend.md) | Le moteur de règles de recommandation vit dans le backend, pas dans `ml/` | +| [0008](adr/0008-airflow-execute-le-code-du-backend.md) | Airflow exécute le code du backend en sous-processus, dans son propre environnement | diff --git a/docs/adr/0008-airflow-execute-le-code-du-backend.md b/docs/adr/0008-airflow-execute-le-code-du-backend.md new file mode 100644 index 0000000..98e50c1 --- /dev/null +++ b/docs/adr/0008-airflow-execute-le-code-du-backend.md @@ -0,0 +1,79 @@ +# 0008 - Airflow exécute le code du backend en sous-processus + +- Statut : accepté +- Date : 2026-09-21 + +## Contexte + +L'issue #116 demande un DAG d'alertes. Ce qu'il a à ordonnancer existe déjà et n'est pas à +réécrire : `AlertService.detect()` et ses cinq règles (#104), puis le moteur de recommandations +(#38). Les deux vivent dans `apps/backend/app/`, et +l'[ADR 0006](0006-moteur-de-regles-dans-le-backend.md) a précisément décidé qu'ils y restent parce +qu'ils s'appuient sur les repositories ORM de l'API plutôt que sur du SQL brut. Les deux +sont décrits par la documentation comme « lancés à la main ». + +L'image Airflow livrée par #115 ne porte que `ml/`, dans un environnement `uv` distinct +(`/opt/ml/.venv`, Python 3.14) de celui d'Airflow lui-même (Python 3.12, contraint par +apache-airflow 2.10). Les DAGs `ml_train` et `ml_score` shellent vers cet environnement. Rien +d'équivalent n'existe pour `apps/backend` : un `BashOperator` sur +`python -m app.detection.internal_alerts` échouerait en `ModuleNotFoundError`. + +## Décision + +**L'image Airflow porte un troisième environnement, `/opt/backend/.venv`**, construit depuis le +`pyproject.toml`, le `uv.lock` et le paquet `app/` du backend. Le DAG `alertes` shelle vers lui +exactement comme `ml_score` shelle vers `/opt/ml/.venv`. + +Trois raisons : + +- **Le patron existe et vient d'être revu.** #115 a posé `BashOperator` + `uv run --no-sync` + + `env -u VIRTUAL_ENV`, avec les tests d'intégrité qui le verrouillent. Introduire une seconde + forme d'appel dans le même dossier `dags/` coûterait plus cher à lire qu'un second environnement + dans le même `Dockerfile`. +- **Aucune surface réseau n'est ajoutée.** La détection n'a pas de route HTTP, contrairement à la + génération de recommandations (`POST /recommendations/generate`, rôle `admin`). En créer une pour + qu'Airflow l'appelle donnerait à l'ordonnanceur un compte administrateur de l'API, en plus des + identifiants PostgreSQL complets qu'il détient déjà, et ferait dépendre la production d'alertes + de la disponibilité du conteneur `backend`. +- **La logique reste où l'ADR 0006 l'a mise.** Le DAG n'apprend rien du domaine : ni les seuils, ni + les cinq règles, ni les clés d'idempotence. Il ne sait que l'heure à laquelle appeler. + +## Conséquences + +- **Airflow reçoit une `APP_SECRET_KEY` délibérément distincte de celle de l'API.** La + configuration du backend refuse de se construire sans elle (`app/core/config.py`), et + `internal_alerts.main()` appelle `get_settings()` avant toute requête pour échouer tôt. Mais la + détection ne signe ni ne vérifie aucun jeton, et Airflow permet d'exécuter du code arbitraire + depuis son interface : un Airflow compromis ne doit pas livrer la clé de signature des JWT. D'où + `AIRFLOW_APP_SECRET_KEY`, avec sa propre garde dans `airflow-init`. +- **`DATABASE_URL`, en dialecte asyncpg, rejoint `ML_DATABASE_URL`** dans l'environnement du + conteneur. Le cantonnement des rôles PostgreSQL reste la dette de + l'[ADR 0003](0003-autorisation-rbac-a-trois-roles.md), et cette décision l'alourdit d'un + consommateur de plus. +- **La CI Airflow se déclenche sur les changements du backend.** L'image le `COPY` : sans + `apps/backend/app/**`, `pyproject.toml` et `uv.lock` dans les déclencheurs du workflow, une + dépendance modifiée casserait la construction sans que rien ne le signale avant le déploiement. + En contrepartie, l'image grossit de ce que pèsent SQLAlchemy, asyncpg et pandas. +- **Aucune variable ne départage les deux environnements, et c'est voulu.** `uv` place par défaut + le venv d'un projet dans `/.venv` : `cd /opt/ml` ou `cd /opt/backend` suffit à choisir le + bon. L'image ne pose donc plus de `UV_PROJECT_ENVIRONMENT` global, hérité de #115 : il vaudrait + pour les deux projets, et `uv run` dans l'un résoudrait le venv de l'autre. Le symptôme n'est pas + une construction ratée mais un `ModuleNotFoundError` à la première tâche, d'où la vérification + d'import sans réseau que la CI fait maintenant sur chacun des deux. +- Airflow lui-même reste étranger au domaine : ni LightGBM, ni SQLAlchemy, ni FastAPI n'entrent + dans son interpréteur. C'est la propriété que #115 avait établie, et elle tient toujours. + +## Alternatives écartées + +- **Route HTTP `POST /alerts/detect` réservée `admin`, appelée par le DAG.** L'image ne bougeait + pas, mais Airflow détenait alors un compte administrateur de l'API, la détection devenait + tributaire du conteneur `backend`, et l'API gagnait une route d'écriture dont aucun client + humain n'a l'usage. À rouvrir si un jour un tiers doit déclencher la détection. +- **`DockerOperator` lançant l'image du backend.** Demande la socket Docker de l'hôte dans le + conteneur Airflow, c'est-à-dire un équivalent root sur la machine, pour un service qui permet + déjà d'exécuter du code depuis son interface. Le provider n'est d'ailleurs pas installé. +- **Réécrire les cinq règles en SQL dans le DAG.** Contredit frontalement l'ADR 0006, duplique le + domaine, et fait diverger les deux copies au premier changement de seuil. +- **Monter `apps/backend` en volume plutôt que le copier.** L'environnement ne serait plus figé à + la construction, `uv` resynchroniserait au premier lancement, et la CI ne prouverait plus rien + de ce qui tourne réellement. diff --git a/docs/architecture/00-vue-ensemble.md b/docs/architecture/00-vue-ensemble.md index b031a50..5eda630 100644 --- a/docs/architecture/00-vue-ensemble.md +++ b/docs/architecture/00-vue-ensemble.md @@ -67,9 +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 `airflow --> db` est maintenant en trait plein : trois DAGs tournent, deux pour +l'entraînement et le scoring du modèle ML (issue #115), un pour la détection d'alertes et la +génération des recommandations (issue #116), 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. @@ -84,7 +85,7 @@ 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`, 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 (EC06, #44/#45) pas encore construite | | 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` | `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 | +| ETL | Apache Airflow | `etl/airflow` | `En cours` | Webserver + scheduler (LocalExecutor) tournent via docker-compose, base de métadonnées Postgres dédiée. Trois DAGs en sous-processus `uv run` : `ml_train` manuel et `ml_score` `@hourly` pour le pipeline ML (issue #115), `alertes` à `15 * * * *` pour la détection et les recommandations (issue #116, [ADR 0008](../adr/0008-airflow-execute-le-code-du-backend.md)). L'ingestion (issues #15/#16) n'a pas encore de DAG | | CI/CD | GitHub Actions | `.github/workflows` | `Cible` | Rien | ## Flux bout en bout diff --git a/docs/architecture/10-infra.md b/docs/architecture/10-infra.md index 1c44eb0..a74855a 100644 --- a/docs/architecture/10-infra.md +++ b/docs/architecture/10-infra.md @@ -50,7 +50,7 @@ Trois pièges sont documentés en tête du `docker-compose.yml`, ils ne se devin - `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) +### Airflow (issues #115 et #116) Trois services, `docker compose profiles` non utilisés (démarrage explicite via `make airflow-up`, pas dans `make dev`) : @@ -59,13 +59,30 @@ airflow-up`, pas dans `make dev`) : |---|---|---| | `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` | +| `airflow-scheduler` | Planifie et **exécute** les tâches (`LocalExecutor`) | Les DAGs y tournent en sous-processus (`uv run --no-sync python -m ...`), 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. +l'image doit pouvoir `COPY` les sources de `ml/` **et** de `apps/backend/` pour se synchroniser +deux environnements Python **3.14** (`/opt/ml/.venv` et `/opt/backend/.venv`, `uv sync --locked` à +la construction), distincts du Python 3.12 qui fait tourner Airflow lui-même. Les DAGs shellent +vers ces venvs plutôt que d'importer LightGBM, MLflow ou SQLAlchemy dans le process Airflow. +Le choix et ses contreparties sont dans +l'[ADR 0008](../adr/0008-airflow-execute-le-code-du-backend.md). + +| DAG | Planification | Ce qu'il lance, et où | +|---|---|---| +| `ml_train` | manuelle | `enervision_ml.train`, dans `/opt/ml/.venv` | +| `ml_score` | `0 * * * *` | `enervision_ml.score`, dans `/opt/ml/.venv` | +| `alertes` | `15 * * * *` | `app.detection.internal_alerts` puis `app.cli generate-recommendations`, dans `/opt/backend/.venv` | + +**Pourquoi `alertes` tourne à la quinzième minute.** Sa règle `anomaly` compare une lecture à la +`prediction` du même instant, que `ml_score` écrit à l'heure pile. Le décalage laisse le scoring +finir. Aucune dépendance n'est déclarée entre les deux DAGs pour autant, ni `ExternalTaskSensor` ni +tâche greffée : quatre règles de détection sur cinq ne touchent pas au modèle, et un modèle jamais +entraîné ne doit pas priver le parc de ses alertes. Ses deux tâches s'enchaînent en revanche +(`recommendation.alert_id` est une clé étrangère `NOT NULL`), et toutes deux sont rejouables sans +risque : l'idempotence est portée par la base, `uq_alert_source_reference` et +`uq_recommendation_alert_rule`. `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 @@ -76,7 +93,15 @@ 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). +`airflow-init` qui refuse de démarrer (clé Fernet, clé Flask, mot de passe ou +`AIRFLOW_APP_SECRET_KEY` vides). + +Le conteneur reçoit deux variables du backend en plus de `ML_DATABASE_URL` : `DATABASE_URL`, en +dialecte asyncpg, et `APP_SECRET_KEY`, alimentée par `AIRFLOW_APP_SECRET_KEY`. Cette dernière est +**délibérément différente** de celle de l'API. La configuration du backend refuse de se construire +sans clé, mais la détection ne signe ni ne vérifie aucun jeton : un Airflow compromis, qui permet +déjà d'exécuter du code depuis son interface, ne doit pas livrer par-dessus la clé de signature +des JWT. **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 @@ -85,8 +110,9 @@ déclenchement reste humain. `ml_score`, lui, est planifié à l'heure, avec `ma (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. +tests d'intégrité des DAGs, et construit l'image (elle `COPY` `ml/` et `apps/backend/`, une +modification de l'un ou de l'autre peut donc la casser, d'où leurs chemins dans les déclencheurs) +avant de vérifier que les deux environnements s'y importent 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 diff --git a/docs/architecture/20-backend.md b/docs/architecture/20-backend.md index b9abfa4..2c52815 100644 --- a/docs/architecture/20-backend.md +++ b/docs/architecture/20-backend.md @@ -256,12 +256,19 @@ 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). -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 -jusqu'ici rien à référencer côté `source="enervision"`. +La détection s'exécute 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`) : `uv run python -m app.detection.internal_alerts [--site-id ...] +[--now ...]`, ou `make detect-alerts`. Cette issue (#104) débloquait #38 (moteur de règles pour +recommandations), dont la FK `alert_id` `NOT NULL` n'avait jusqu'ici rien à référencer côté +`source="enervision"`. + +Depuis l'issue #116, le lancement n'est plus manuel : le DAG Airflow `alertes` enchaîne cette +détection et la génération des recommandations, toutes les heures à la quinzième minute. Airflow +exécute le code du backend en sous-processus, dans son propre environnement, ce que décide +l'[ADR 0008](../adr/0008-airflow-execute-le-code-du-backend.md) ; le détail de l'ordonnancement est +dans [10-infra.md](10-infra.md). La ligne de commande reste le moyen de rejouer une fenêtre +passée, ce que `--now` permet et que le DAG ne fait pas. ### `/health/ready` diff --git a/docs/architecture/40-data.md b/docs/architecture/40-data.md index 47fdc8a..82f769b 100644 --- a/docs/architecture/40-data.md +++ b/docs/architecture/40-data.md @@ -14,8 +14,10 @@ décrivent les éléments prévus mais pas encore réalisés. L'ingestion des **mesures** est implémentée pour les deux sources du MVP, le dataset CSV/JSON et l'API Mock. Celle des **alertes** de l'API Mock, `/alerts`, reste à faire : voir -l'[ADR 0006](../adr/0006-moteur-de-regles-dans-le-backend.md). L'orchestration Airflow, les -agrégats continus, la compression et la rétention restent des cibles. +l'[ADR 0006](../adr/0006-moteur-de-regles-dans-le-backend.md). Les alertes `source='enervision'`, +elles, sont produites par la détection interne, désormais ordonnancée par le DAG Airflow `alertes` +(issue #116). L'orchestration de l'ingestion, les agrégats continus, la compression et la +rétention restent des cibles. ## Trois emplacements, trois rôles @@ -290,10 +292,10 @@ Les anomalies historiques décrites dans les JSON sont conservées dans `dataset Elles servent à l'analyse des données et ne sont pas considérées comme des alertes actuelles. Les lignes de `recommendation` sont écrites par le moteur de règles du backend -(`app/services/recommendation_rules.py`), déclenché par `POST /api/v1/recommendations/generate` -ou par `make recommendations`, à partir des alertes déjà en base. Le couple -`(alert_id, rule_reference)` est unique : rejouer le moteur sur les mêmes alertes n'ajoute aucune -ligne. +(`app/services/recommendation_rules.py`), déclenché par `POST /api/v1/recommendations/generate`, +par `make recommendations`, ou par la seconde tâche du DAG `alertes`, à partir des alertes déjà en +base. Le couple `(alert_id, rule_reference)` est unique : rejouer le moteur sur les mêmes alertes +n'ajoute aucune ligne. ### Relations entre les tables diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 78a82c9..1e3a260 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -17,8 +17,11 @@ 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 | L'observabilité et la CI/CD n'ont pas de document propre : ce sont des sections des documents -ci-dessus, tant que `monitoring/` et `etl/airflow/` ne contiennent que des `.gitkeep`. Elles en -sortiront le jour où elles auront de la matière. Un fichier vide de plus n'aide personne. +ci-dessus, tant que `monitoring/` ne contient que des `.gitkeep`. Elles en sortiront le jour où +elles auront de la matière. Un fichier vide de plus n'aide personne. + +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). La sécurité applicative, elle, a désormais de la matière : la vue consolidée reste dans [00-vue-ensemble.md](00-vue-ensemble.md), le détail dans [20-backend.md](20-backend.md), la diff --git a/docs/architecture/owasp-traceabilite.md b/docs/architecture/owasp-traceabilite.md index 132e107..967791d 100644 --- a/docs/architecture/owasp-traceabilite.md +++ b/docs/architecture/owasp-traceabilite.md @@ -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. 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`. | +| **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`. Aggravée par #116 : le conteneur reçoit aussi `DATABASE_URL` et exécute le code du backend en sous-processus (ADR 0008). Atténuations en place : le compte admin Airflow est distinct des `app_user` et son mot de passe passe par l'environnement, jamais par `argv` ; et l'`APP_SECRET_KEY` donnée à Airflow est distincte de celle de l'API, pour qu'une compromission ne livre pas la clé de signature des JWT. | | **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 diff --git a/etl/README.md b/etl/README.md index b442ae3..698ffca 100644 --- a/etl/README.md +++ b/etl/README.md @@ -663,8 +663,8 @@ mock_api_import.py La logique d'extraction, de transformation et de chargement est donc disponible pour les deux sources de données du MVP. -Airflow tourne désormais réellement (`etl/airflow/`, `make airflow-up`), mais il orchestre pour l'instant le pipeline ML (`ml_train`/`ml_score`, issue #115), pas encore ces deux imports : orchestrer `historical_import.py` et `mock_api_import.py` (normalisation et chargement micro-batch, issues #15/#16) reste à faire. +Airflow tourne désormais réellement (`etl/airflow/`, `make airflow-up`) et orchestre le pipeline ML (`ml_train`/`ml_score`, issue #115) ainsi que la détection d'alertes et la génération des recommandations (`alertes`, issue #116). Il n'orchestre pas encore ces deux imports : `historical_import.py` et `mock_api_import.py` (normalisation et chargement micro-batch, issues #15/#16) restent à faire. -Airflow permet de planifier les traitements, gérer leur ordre d'exécution, suivre leur état et remonter les erreurs. Il ne remplace pas la logique ETL Python existante : les scripts actuels restent responsables de l'extraction, de la validation, de la transformation et du chargement. `etl/airflow/dags/ml_train.py` et `ml_score.py` montrent le patron retenu (des `BashOperator` qui invoquent le script tel quel). +Airflow permet de planifier les traitements, gérer leur ordre d'exécution, suivre leur état et remonter les erreurs. Il ne remplace pas la logique ETL Python existante : les scripts actuels restent responsables de l'extraction, de la validation, de la transformation et du chargement. `etl/airflow/dags/ml_train.py`, `ml_score.py` et `alertes.py` montrent le patron retenu (des `BashOperator` qui invoquent le script tel quel, dans l'environnement `uv` que l'image embarque pour lui). Le pipeline Data servira ensuite à préparer les données nécessaires au modèle de Machine Learning.