Merge pull request #135 from ineszang/dependabot/uv/etl/airflow/apache-airflow-3.3.0
chore(deps): bump apache-airflow from 2.10.4 to 3.3.0 in /etl/airflow
This commit is contained in:
+6
-3
@@ -29,15 +29,18 @@ APP_MOCK_API_USERNAME=change_me
|
|||||||
APP_MOCK_API_PASSWORD=change_me
|
APP_MOCK_API_PASSWORD=change_me
|
||||||
APP_MOCK_API_TIMEOUT_SECONDS=10
|
APP_MOCK_API_TIMEOUT_SECONDS=10
|
||||||
|
|
||||||
# Airflow (webserver + scheduler, LocalExecutor). Base de métadonnées dédiée `airflow` dans le
|
# Airflow (api-server + scheduler + dag-processor, LocalExecutor). Base de métadonnées dédiée `airflow` dans le
|
||||||
# même conteneur `db` (cf. db/init/120-airflow-database.sql), pas un conteneur de plus.
|
# même conteneur `db` (cf. db/init/120-airflow-database.sql), pas un conteneur de plus.
|
||||||
AIRFLOW_PORT=8080
|
AIRFLOW_PORT=8080
|
||||||
# Chiffre les connexions/variables stockées par Airflow. Générer la vôtre :
|
# Chiffre les connexions/variables stockées par Airflow. Générer la vôtre :
|
||||||
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
||||||
AIRFLOW_FERNET_KEY=change_me
|
AIRFLOW_FERNET_KEY=change_me
|
||||||
# Clé Flask du webserver Airflow (signature de session), distincte de la précédente. Générer la
|
# Clé de session de l'api-server Airflow, distincte de la précédente. Générer la
|
||||||
# vôtre : python -c "import secrets; print(secrets.token_urlsafe(48))"
|
# vôtre : python -c "import secrets; print(secrets.token_urlsafe(48))"
|
||||||
AIRFLOW_WEBSERVER_SECRET_KEY=change_me
|
AIRFLOW_API_SECRET_KEY=change_me
|
||||||
|
# Secret des jetons JWT entre scheduler, tâches et api-server (conteneurs distincts, le secret
|
||||||
|
# doit être partagé). Même générateur que ci-dessus.
|
||||||
|
AIRFLOW_JWT_SECRET=change_me
|
||||||
AIRFLOW_ADMIN_USERNAME=admin
|
AIRFLOW_ADMIN_USERNAME=admin
|
||||||
# Compte Airflow créé au premier démarrage (service `airflow-init`), sans rapport avec les
|
# Compte Airflow créé au premier démarrage (service `airflow-init`), sans rapport avec les
|
||||||
# comptes `app_user` d'EnerVision.
|
# comptes `app_user` d'EnerVision.
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
name: Airflow
|
name: Airflow
|
||||||
|
|
||||||
# Piège : la version de Python vient de etl/airflow/.python-version. C'est 3.12 et non 3.14
|
# Piège : la version de Python vient de etl/airflow/.python-version. C'est 3.12 et non 3.14
|
||||||
# (contrairement à backend.yml et ml.yml) : apache-airflow 2.10 ne supporte pas 3.14. Le 3.14 de
|
# (contrairement à backend.yml et ml.yml) : celui de l'image apache/airflow retenue, et les tests
|
||||||
# ml/ ne vit que dans l'image Docker, dans son propre environnement (cf. etl/airflow/Dockerfile).
|
# doivent tourner sur le même interpréteur qu'elle. Le 3.14 de ml/ ne vit que dans l'image
|
||||||
|
# Docker, dans son propre environnement (cf. etl/airflow/Dockerfile).
|
||||||
#
|
#
|
||||||
# Piège : l'image COPY les fichiers de dépendances et le code de ml/ et de apps/backend/. Une
|
# Piège : l'image COPY les fichiers de dépendances et le code de ml/ et de apps/backend/. Une
|
||||||
# modification de l'un ou de l'autre peut donc casser sa construction, d'où ces chemins dans
|
# modification de l'un ou de l'autre peut donc casser sa construction, d'où ces chemins dans
|
||||||
|
|||||||
@@ -108,12 +108,12 @@ airflow-test: ## Verifie que les DAGs s'importent sans erreur et ont la structur
|
|||||||
|
|
||||||
airflow-check: airflow-lint airflow-test ## Chaîne de vérification complète des DAGs Airflow
|
airflow-check: airflow-lint airflow-test ## Chaîne de vérification complète des DAGs Airflow
|
||||||
|
|
||||||
airflow-up: ## Démarre Airflow (webserver + scheduler, LocalExecutor). db-up requis avant.
|
airflow-up: ## Démarre Airflow (api-server + scheduler + dag-processor, LocalExecutor). db-up requis avant.
|
||||||
docker compose up -d airflow-init airflow-webserver airflow-scheduler
|
docker compose up -d airflow-init airflow-apiserver airflow-scheduler airflow-dag-processor
|
||||||
@echo "airflow -> http://localhost:$${AIRFLOW_PORT:-8080}"
|
@echo "airflow -> http://localhost:$${AIRFLOW_PORT:-8080}"
|
||||||
|
|
||||||
airflow-down: ## Arrête le webserver et le scheduler Airflow
|
airflow-down: ## Arrête l'api-server, le scheduler et le dag-processor Airflow
|
||||||
docker compose stop airflow-webserver airflow-scheduler
|
docker compose stop airflow-apiserver airflow-scheduler airflow-dag-processor
|
||||||
|
|
||||||
airflow-logs: ## Suit les journaux du scheduler Airflow (où tournent les tâches, LocalExecutor)
|
airflow-logs: ## Suit les journaux du scheduler Airflow (où tournent les tâches, LocalExecutor)
|
||||||
docker compose logs -f airflow-scheduler
|
docker compose logs -f airflow-scheduler
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
-- Base de metadonnees Airflow (webserver + scheduler, LocalExecutor). Separee de la base
|
-- Base de metadonnees Airflow (api-server + scheduler + dag-processor, LocalExecutor). Separee de la base
|
||||||
-- applicative : les tables internes d'Airflow (dag_run, task_instance, ...) n'ont rien a faire
|
-- applicative : les tables internes d'Airflow (dag_run, task_instance, ...) n'ont rien a faire
|
||||||
-- dans le schema metier. Meme conteneur Postgres que `enervision`/`enervision_test` plutot qu'un
|
-- dans le schema metier. Meme conteneur Postgres que `enervision`/`enervision_test` plutot qu'un
|
||||||
-- service dedie, pour ne pas ajouter un conteneur de plus (issue #115).
|
-- service dedie, pour ne pas ajouter un conteneur de plus (issue #115).
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ services:
|
|||||||
ports: !override
|
ports: !override
|
||||||
- "127.0.0.1:${MAILPIT_UI_PORT:-8025}:8025"
|
- "127.0.0.1:${MAILPIT_UI_PORT:-8025}:8025"
|
||||||
|
|
||||||
airflow-webserver:
|
airflow-apiserver:
|
||||||
ports: !override
|
ports: !override
|
||||||
- "127.0.0.1:${AIRFLOW_PORT:-8080}:8080"
|
- "127.0.0.1:${AIRFLOW_PORT:-8080}:8080"
|
||||||
|
|
||||||
|
|||||||
+31
-13
@@ -5,9 +5,9 @@
|
|||||||
|
|
||||||
name: enervision
|
name: enervision
|
||||||
|
|
||||||
# Piege : LocalExecutor fait tourner les taches comme sous-processus du scheduler, jamais du
|
# Piege : LocalExecutor fait tourner les taches comme sous-processus du scheduler, jamais de
|
||||||
# webserver. `airflow_ml_state` (modele entraine, magasin MLflow) n'a donc besoin d'etre monte
|
# l'api-server. `airflow_ml_state` (modele entraine, magasin MLflow) n'a donc besoin d'etre monte
|
||||||
# que sur `airflow-scheduler` en pratique, mais reste partage avec le webserver pour que ce
|
# que sur `airflow-scheduler` en pratique, mais reste partage avec l'api-server pour que ce
|
||||||
# dernier puisse au besoin l'inspecter sans en devenir dependant.
|
# dernier puisse au besoin l'inspecter sans en devenir dependant.
|
||||||
x-airflow-common: &airflow-common
|
x-airflow-common: &airflow-common
|
||||||
build:
|
build:
|
||||||
@@ -19,9 +19,16 @@ x-airflow-common: &airflow-common
|
|||||||
# Piege : pas de `:?` sur les secrets Airflow. Compose interpole le fichier entier avant de
|
# Piege : pas de `:?` sur les secrets Airflow. Compose interpole le fichier entier avant de
|
||||||
# filtrer les services : une variable requise manquante casserait aussi `make db-up`,
|
# filtrer les services : une variable requise manquante casserait aussi `make db-up`,
|
||||||
# `make dev`... pour quiconque n'a pas encore complete son `.env`. Le refus est porte par
|
# `make dev`... pour quiconque n'a pas encore complete son `.env`. Le refus est porte par
|
||||||
# `airflow-init` (ci-dessous), dont `webserver` et `scheduler` dependent.
|
# `airflow-init` (ci-dessous), dont `api-server`, `dag-processor` et `scheduler` dependent.
|
||||||
AIRFLOW__CORE__FERNET_KEY: ${AIRFLOW_FERNET_KEY:-}
|
AIRFLOW__CORE__FERNET_KEY: ${AIRFLOW_FERNET_KEY:-}
|
||||||
AIRFLOW__WEBSERVER__SECRET_KEY: ${AIRFLOW_WEBSERVER_SECRET_KEY:-}
|
AIRFLOW__API__SECRET_KEY: ${AIRFLOW_API_SECRET_KEY:-}
|
||||||
|
# Signe les jetons entre scheduler, tâches et api-server. Conteneurs distincts : un secret
|
||||||
|
# généré au démarrage ne serait pas partagé, il doit venir du .env.
|
||||||
|
AIRFLOW__API_AUTH__JWT_SECRET: ${AIRFLOW_JWT_SECRET:-}
|
||||||
|
AIRFLOW__CORE__EXECUTION_API_SERVER_URL: http://airflow-apiserver:8080/execution/
|
||||||
|
# FabAuthManager plutôt que le SimpleAuthManager par défaut d'Airflow 3 : seul le provider
|
||||||
|
# FAB sait créer le compte admin que `airflow-init` pose via `_AIRFLOW_WWW_USER_*`.
|
||||||
|
AIRFLOW__CORE__AUTH_MANAGER: airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager
|
||||||
AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql+psycopg2://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/airflow
|
AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql+psycopg2://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/airflow
|
||||||
# Role `enervision_ml` dedie pas encore provisionne (dette assumee, cf. ADR 0003) :
|
# Role `enervision_ml` dedie pas encore provisionne (dette assumee, cf. ADR 0003) :
|
||||||
# memes identifiants que le backend en attendant.
|
# memes identifiants que le backend en attendant.
|
||||||
@@ -110,11 +117,11 @@ services:
|
|||||||
# Conteneur unique, jamais redemarre. La migration et la creation du premier compte sont
|
# Conteneur unique, jamais redemarre. La migration et la creation du premier compte sont
|
||||||
# portees par l'entrypoint de l'image (`_AIRFLOW_DB_MIGRATE`, `_AIRFLOW_WWW_USER_*`), qui porte
|
# portees par l'entrypoint de l'image (`_AIRFLOW_DB_MIGRATE`, `_AIRFLOW_WWW_USER_*`), qui porte
|
||||||
# aussi leur code de sortie : une migration ratee (ex. base `airflow` absente sur un volume
|
# aussi leur code de sortie : une migration ratee (ex. base `airflow` absente sur un volume
|
||||||
# `pgdata` deja peuple) fait echouer ce service, et `webserver`/`scheduler`, qui attendent son
|
# `pgdata` deja peuple) fait echouer ce service, et api-server, dag-processor et scheduler,
|
||||||
# succes, ne demarrent pas sur une base non migree. Le mot de passe passe par l'environnement,
|
# qui attendent son succes, ne demarrent pas sur une base non migree. Le mot de passe passe
|
||||||
# jamais par `argv` (ni `ps`, ni `docker compose config`).
|
# par l'environnement, jamais par `argv` (ni `ps`, ni `docker compose config`).
|
||||||
# Sans mot de passe, l'entrypoint refuse lui-meme de creer le compte ; la commande ci-dessous
|
# Sans mot de passe, l'entrypoint refuse lui-meme de creer le compte ; la commande ci-dessous
|
||||||
# refuse en plus les deux cles de chiffrement vides.
|
# refuse en plus les cles et secrets vides.
|
||||||
airflow-init:
|
airflow-init:
|
||||||
<<: *airflow-common
|
<<: *airflow-common
|
||||||
restart: "no"
|
restart: "no"
|
||||||
@@ -134,13 +141,14 @@ services:
|
|||||||
- |
|
- |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
: "$${AIRFLOW__CORE__FERNET_KEY:?AIRFLOW_FERNET_KEY manquant dans .env}"
|
: "$${AIRFLOW__CORE__FERNET_KEY:?AIRFLOW_FERNET_KEY manquant dans .env}"
|
||||||
: "$${AIRFLOW__WEBSERVER__SECRET_KEY:?AIRFLOW_WEBSERVER_SECRET_KEY manquant dans .env}"
|
: "$${AIRFLOW__API__SECRET_KEY:?AIRFLOW_API_SECRET_KEY manquant dans .env}"
|
||||||
|
: "$${AIRFLOW__API_AUTH__JWT_SECRET:?AIRFLOW_JWT_SECRET manquant dans .env}"
|
||||||
: "$${APP_SECRET_KEY:?AIRFLOW_APP_SECRET_KEY manquant dans .env}"
|
: "$${APP_SECRET_KEY:?AIRFLOW_APP_SECRET_KEY manquant dans .env}"
|
||||||
exec airflow version
|
exec airflow version
|
||||||
|
|
||||||
airflow-webserver:
|
airflow-apiserver:
|
||||||
<<: *airflow-common
|
<<: *airflow-common
|
||||||
command: webserver
|
command: api-server
|
||||||
ports:
|
ports:
|
||||||
- "${AIRFLOW_PORT:-8080}:8080"
|
- "${AIRFLOW_PORT:-8080}:8080"
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -149,7 +157,7 @@ services:
|
|||||||
airflow-init:
|
airflow-init:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "--fail", "http://localhost:8080/health"]
|
test: ["CMD", "curl", "--fail", "http://localhost:8080/api/v2/monitor/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 5
|
retries: 5
|
||||||
@@ -164,6 +172,16 @@ services:
|
|||||||
airflow-init:
|
airflow-init:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
|
||||||
|
# Obligatoire depuis Airflow 3 : le scheduler ne parse plus les fichiers de dags/ lui-même.
|
||||||
|
airflow-dag-processor:
|
||||||
|
<<: *airflow-common
|
||||||
|
command: dag-processor
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
airflow-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
airflow_logs:
|
airflow_logs:
|
||||||
|
|||||||
@@ -48,20 +48,28 @@ Trois pièges sont documentés en tête du `docker-compose.yml`, ils ne se devin
|
|||||||
- `db/init` est monté **fichier par fichier**. Monter le dossier masquerait les scripts d'init de
|
- `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
|
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).
|
le compose. Voir [`db/README.md`](../../db/README.md).
|
||||||
- `LocalExecutor` exécute les tâches comme sous-processus du **scheduler**, jamais du webserver :
|
- `LocalExecutor` exécute les tâches comme sous-processus du **scheduler**, jamais de l'api-server :
|
||||||
c'est le scheduler qui a besoin du volume `airflow_ml_state` (modèle, magasin MLflow).
|
c'est le scheduler qui a besoin du volume `airflow_ml_state` (modèle, magasin MLflow).
|
||||||
|
|
||||||
### Airflow (issues #115 et #116)
|
### Airflow (issues #115 et #116)
|
||||||
|
|
||||||
Trois services, `docker compose profiles` non utilisés (démarrage explicite via `make
|
Quatre services (Airflow 3.3), `docker compose profiles` non utilisés (démarrage explicite via `make
|
||||||
airflow-up`, pas dans `make dev`) :
|
airflow-up`, pas dans `make dev`) :
|
||||||
|
|
||||||
| Service | Rôle | Points notables |
|
| 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-init` | Migre la base de métadonnées, crée le compte admin | Conteneur jetable (`restart: "no"`), ne redémarre jamais. `api-server`, `dag-processor` et `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-apiserver` | UI et API REST (`/api/v2`), port `8080` | `LocalExecutor` : n'exécute aucune tâche lui-même. Sert aussi l'Execution API que les tâches appellent, d'où le secret JWT partagé |
|
||||||
|
| `airflow-dag-processor` | Parse `dags/` et publie les DAGs sérialisés | Composant à part entière depuis Airflow 3 : le scheduler ne lit plus les fichiers de DAG |
|
||||||
| `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` |
|
| `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` |
|
||||||
|
|
||||||
|
Airflow 3 impose deux choses que le compose reflète : les tâches ne touchent plus la base de
|
||||||
|
métadonnées et passent par l'Execution API de l'`api-server`, avec un jeton signé par
|
||||||
|
`AIRFLOW_JWT_SECRET` (secret partagé entre conteneurs, jamais celui généré au démarrage) ; et
|
||||||
|
l'authentification par défaut (`SimpleAuthManager`) ne sait pas créer de compte, d'où le
|
||||||
|
`FabAuthManager` qui garde le compte admin posé par `airflow-init`. Pas de `triggerer` : aucun
|
||||||
|
opérateur déférable dans les DAGs.
|
||||||
|
|
||||||
Construits depuis `etl/airflow/Dockerfile`, contexte `.` (racine du repo, pas `etl/airflow/`) :
|
Construits depuis `etl/airflow/Dockerfile`, contexte `.` (racine du repo, pas `etl/airflow/`) :
|
||||||
l'image doit pouvoir `COPY` les sources de `ml/` **et** de `apps/backend/` pour se synchroniser
|
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` à
|
deux environnements Python **3.14** (`/opt/ml/.venv` et `/opt/backend/.venv`, `uv sync --locked` à
|
||||||
@@ -95,14 +103,14 @@ rend contraignant.
|
|||||||
`airflow-init` s'appuie sur l'entrypoint de l'image (`_AIRFLOW_DB_MIGRATE`,
|
`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
|
`_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
|
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
|
`api-server`, `dag-processor` et `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`).
|
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
|
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`,
|
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
|
`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, mot de passe ou
|
`airflow-init` qui refuse de démarrer (clé Fernet, clé de session de l'API, secret JWT, mot de
|
||||||
`AIRFLOW_APP_SECRET_KEY` vides).
|
passe ou `AIRFLOW_APP_SECRET_KEY` vides).
|
||||||
|
|
||||||
Le conteneur reçoit deux variables du backend en plus de `ML_DATABASE_URL` : `DATABASE_URL`, en
|
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
|
dialecte asyncpg, et `APP_SECRET_KEY`, alimentée par `AIRFLOW_APP_SECRET_KEY`. Cette dernière est
|
||||||
@@ -242,7 +250,7 @@ Ces arbitrages sont pris. Ils ne vivaient jusqu'ici que dans des commentaires de
|
|||||||
| Base applicative | `enervision` | Variable `POSTGRES_DB` |
|
| 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 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` |
|
| 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 |
|
| API server Airflow | `8080` | `make airflow-up`. Api-server, scheduler et dag-processor ne publient que ce port ; les tâches (`LocalExecutor`) tournent côté scheduler, sans port propre |
|
||||||
|
|
||||||
## Le trou vers k3s
|
## Le trou vers k3s
|
||||||
|
|
||||||
|
|||||||
@@ -81,9 +81,10 @@ rien changer), mais ce serait à borner sur un dépôt à forte fréquence de pu
|
|||||||
`backend.yml`, `ml.yml` et `airflow.yml` déclarent en plus un groupe de concurrence par référence
|
`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.
|
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
|
**Piège de version** : `etl/airflow` tourne en **Python 3.12** et non 3.14 : c'est l'interpréteur
|
||||||
ne supporte pas encore 3.14. Le 3.14 du module ML ne vit, dans ce contexte, que dans l'image
|
de l'image `apache/airflow:3.3.2-python3.12` retenue, et les tests d'intégrité doivent tourner sur
|
||||||
Docker et son propre environnement.
|
le même. 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
|
## Ce qui bloque un merge
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Image Airflow EnerVision : ajoute ml/ et apps/backend/ dans leurs propres environnements Python
|
# Image Airflow EnerVision : ajoute ml/ et apps/backend/ dans leurs propres environnements Python
|
||||||
# 3.14, distincts du Python 3.12 qui fait tourner Airflow lui-meme (apache-airflow 2.10 ne supporte
|
# 3.14, distincts du Python 3.12 de l'image de base qui fait tourner Airflow lui-meme, pour que
|
||||||
# pas 3.14), pour que les DAGs puissent lancer `uv run python -m enervision_ml.train`/`.score`,
|
# les DAGs puissent lancer `uv run python -m enervision_ml.train`/`.score`,
|
||||||
# `app.detection.internal_alerts` et `app.cli` en sous-processus. Airflow ne devient jamais un
|
# `app.detection.internal_alerts` et `app.cli` en sous-processus. Airflow ne devient jamais un
|
||||||
# consommateur direct de LightGBM, de MLflow ou du SQLAlchemy du backend. Cf. ADR 0008.
|
# consommateur direct de LightGBM, de MLflow ou du SQLAlchemy du backend. Cf. ADR 0008.
|
||||||
FROM apache/airflow:2.10.4-python3.12
|
FROM apache/airflow:3.3.2-python3.12
|
||||||
|
|
||||||
# LightGBM est compile contre libgomp (OpenMP), absent de l'image de base (minimale, sans
|
# LightGBM est compile contre libgomp (OpenMP), absent de l'image de base (minimale, sans
|
||||||
# toolchain de compilation). Sans lui : `OSError: libgomp.so.1: cannot open shared object file`
|
# toolchain de compilation). Sans lui : `OSError: libgomp.so.1: cannot open shared object file`
|
||||||
@@ -21,9 +21,8 @@ RUN apt-get update \
|
|||||||
RUN mkdir -p /opt/ml/state /opt/backend && chown -R airflow:root /opt/ml /opt/backend
|
RUN mkdir -p /opt/ml/state /opt/backend && chown -R airflow:root /opt/ml /opt/backend
|
||||||
USER airflow
|
USER airflow
|
||||||
|
|
||||||
# L'image de base embarque deja un `uv`, mais trop ancien (0.4.29) pour le format de verrou de
|
# L'image de base embarque deja un `uv`, mais pas celui que le depot epingle par ailleurs
|
||||||
# `ml/uv.lock`. On le remplace par la version deja pinnee ailleurs dans le depot
|
# (apps/backend/Dockerfile) : on aligne, pour que le format de verrou lu soit le meme partout.
|
||||||
# (apps/backend/Dockerfile).
|
|
||||||
COPY --from=ghcr.io/astral-sh/uv:0.11.26 /uv /home/airflow/.local/bin/uv
|
COPY --from=ghcr.io/astral-sh/uv:0.11.26 /uv /home/airflow/.local/bin/uv
|
||||||
|
|
||||||
# Piege : pas de `UV_PROJECT_ENVIRONMENT` global. Il vaudrait pour les deux projets, et `uv run`
|
# Piege : pas de `UV_PROJECT_ENVIRONMENT` global. Il vaudrait pour les deux projets, et `uv run`
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from airflow.models.dag import DAG
|
from airflow.providers.standard.operators.bash import BashOperator
|
||||||
from airflow.operators.bash import BashOperator
|
from airflow.sdk import DAG
|
||||||
|
|
||||||
# Le backend a son propre environnement uv dans l'image (ADR 0008). `--no-sync` et
|
# Le backend a son propre environnement uv dans l'image (ADR 0008). `--no-sync` et
|
||||||
# `env -u VIRTUAL_ENV` : cf. `ml_train.py`, même raisonnement.
|
# `env -u VIRTUAL_ENV` : cf. `ml_train.py`, même raisonnement.
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from airflow.models.dag import DAG
|
from airflow.providers.standard.operators.bash import BashOperator
|
||||||
from airflow.operators.bash import BashOperator
|
from airflow.sdk import DAG
|
||||||
|
|
||||||
MODEL_PATH = "/opt/ml/state/models/lightgbm-consumption.txt"
|
MODEL_PATH = "/opt/ml/state/models/lightgbm-consumption.txt"
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from airflow.models.dag import DAG
|
from airflow.providers.standard.operators.bash import BashOperator
|
||||||
from airflow.operators.bash import BashOperator
|
from airflow.sdk import DAG
|
||||||
|
|
||||||
MODEL_PATH = "/opt/ml/state/models/lightgbm-consumption.txt"
|
MODEL_PATH = "/opt/ml/state/models/lightgbm-consumption.txt"
|
||||||
MLFLOW_TRACKING_URI = "sqlite:////opt/ml/state/mlflow.db"
|
MLFLOW_TRACKING_URI = "sqlite:////opt/ml/state/mlflow.db"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
|||||||
description = "DAGs d'orchestration EnerVision (Airflow)"
|
description = "DAGs d'orchestration EnerVision (Airflow)"
|
||||||
requires-python = ">=3.12,<3.13"
|
requires-python = ">=3.12,<3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"apache-airflow==2.10.4",
|
"apache-airflow==3.3.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ from datetime import timedelta
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from airflow.models.baseoperator import BaseOperator
|
from airflow.dag_processing.dagbag import DagBag
|
||||||
from airflow.models.dagbag import DagBag
|
from airflow.sdk import BaseOperator
|
||||||
|
|
||||||
DAGS_FOLDER = Path(__file__).resolve().parent.parent / "dags"
|
DAGS_FOLDER = Path(__file__).resolve().parent.parent / "dags"
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ TACHES = [
|
|||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def dagbag() -> DagBag:
|
def dagbag() -> DagBag:
|
||||||
return DagBag(dag_folder=str(DAGS_FOLDER), include_examples=False)
|
return DagBag(dag_folder=str(DAGS_FOLDER))
|
||||||
|
|
||||||
|
|
||||||
def test_dags_folder_has_no_import_error(dagbag: DagBag) -> None:
|
def test_dags_folder_has_no_import_error(dagbag: DagBag) -> None:
|
||||||
@@ -33,18 +33,18 @@ def test_every_expected_dag_is_discovered(dagbag: DagBag) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_ml_train_has_no_schedule(dagbag: DagBag) -> None:
|
def test_ml_train_has_no_schedule(dagbag: DagBag) -> None:
|
||||||
assert dagbag.dags["ml_train"].timetable.summary == "None"
|
assert dagbag.dags["ml_train"].schedule is None
|
||||||
|
|
||||||
|
|
||||||
def test_ml_score_runs_every_hour(dagbag: DagBag) -> None:
|
def test_ml_score_runs_every_hour(dagbag: DagBag) -> None:
|
||||||
# `@hourly` est un alias Airflow pour ce cron, c'est sous cette forme que `.summary` le rend.
|
# `@hourly` est un alias Airflow pour ce cron, c'est sous cette forme que la timetable le rend.
|
||||||
assert dagbag.dags["ml_score"].timetable.summary == "0 * * * *"
|
assert dagbag.dags["ml_score"].timetable.expression == "0 * * * *"
|
||||||
|
|
||||||
|
|
||||||
def test_alertes_runs_after_the_hourly_scoring(dagbag: DagBag) -> None:
|
def test_alertes_runs_after_the_hourly_scoring(dagbag: DagBag) -> None:
|
||||||
# Le decalage n'est pas cosmetique : la regle `anomaly` compare une lecture a la `prediction`
|
# Le decalage n'est pas cosmetique : la regle `anomaly` compare une lecture a la `prediction`
|
||||||
# du meme instant, que `ml_score` ecrit a l'heure pile.
|
# du meme instant, que `ml_score` ecrit a l'heure pile.
|
||||||
assert dagbag.dags["alertes"].timetable.summary == "15 * * * *"
|
assert dagbag.dags["alertes"].timetable.expression == "15 * * * *"
|
||||||
|
|
||||||
|
|
||||||
def test_ml_train_task_calls_the_training_module(dagbag: DagBag) -> None:
|
def test_ml_train_task_calls_the_training_module(dagbag: DagBag) -> None:
|
||||||
|
|||||||
Generated
+554
-653
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user