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.
5.3 KiB
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 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 dossierdags/coûterait plus cher à lire qu'un second environnement dans le mêmeDockerfile. - 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ôleadmin). 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 conteneurbackend. - 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_KEYdélibérément distincte de celle de l'API. La configuration du backend refuse de se construire sans elle (app/core/config.py), etinternal_alerts.main()appelleget_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 dansairflow-init. DATABASE_URL, en dialecte asyncpg, rejointML_DATABASE_URLdans l'environnement du conteneur. Le cantonnement des rôles PostgreSQL reste la dette de l'ADR 0003, 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: sansapps/backend/app/**,pyproject.tomletuv.lockdans 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.
uvplace par défaut le venv d'un projet dans<projet>/.venv:cd /opt/mloucd /opt/backendsuffit à choisir le bon. L'image ne pose donc plus deUV_PROJECT_ENVIRONMENTglobal, hérité de #115 : il vaudrait pour les deux projets, etuv rundans l'un résoudrait le venv de l'autre. Le symptôme n'est pas une construction ratée mais unModuleNotFoundErrorà 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/detectréservéeadmin, 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 conteneurbackend, 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. DockerOperatorlanç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/backenden volume plutôt que le copier. L'environnement ne serait plus figé à la construction,uvresynchroniserait au premier lancement, et la CI ne prouverait plus rien de ce qui tourne réellement.