docs: acte l'ordonnancement des alertes par l'ADR 0008 et met à jour les vues
L'ADR 0008 décide qu'Airflow exécute le code du backend en sous-processus plutôt que d'appeler l'API, et assume ce que cela coûte : une image plus lourde, la CI Airflow déclenchée par les changements du backend, une clé applicative de plus. Les vues suivent. Trois DAGs dans 10-infra.md et dans la vue d'ensemble, avec le motif du décalage horaire. La détection n'est plus « lancée à la main » dans 20-backend.md. La génération des recommandations gagne son troisième déclencheur dans 40-data.md. La dette de cantonnement ETL et ML porte l'aggravation comme l'atténuation. L'affirmation selon laquelle `etl/airflow/` ne contient que des `.gitkeep`, fausse depuis l'issue #115, disparaît. L'index des décisions omettait les ADR 0005 et 0006, il les récupère au passage.
This commit is contained in:
@@ -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 `<projet>/.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.
|
||||
Reference in New Issue
Block a user