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.
80 lines
5.3 KiB
Markdown
80 lines
5.3 KiB
Markdown
# 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.
|