diff --git a/README.md b/README.md index 2483106..b89796c 100644 --- a/README.md +++ b/README.md @@ -102,3 +102,34 @@ curl -s -X POST localhost:8000/predict/batch \ ## Livrable TP03 Reponses aux questions et recap : `SYNTHESE_TP03.md`. + +--- + +# TP04 - Automatiser le pipeline avec la CI/CD (pipeline.py + Forgejo Actions) + +Prolonge les TP precedents : les etapes manuelles (split -> entrainement -> enregistrement/promotion +Registry) sont orchestrees en une seule commande, puis rejouees automatiquement en CI. + +## Pipeline (Parties 2-3) + +`pipeline.py` (racine) enchaine, via des sous-processus (`check=True`, arret au premier echec) : + +```bash +set -a; source .env; set +a +python pipeline.py # split -> train full --register -> promote champion +``` + +- 1. `lab.split.cli` : (re)cree `data/{train,validation,test}.parquet` (deterministe) ; +- 2. `lab.modeling.cli full --register` : entraine + `log_model` + nouvelle version au Registry ; +- 3. `lab.registry.cli promote` : repointe l'alias `champion` sur cette version. + +`requirements.txt` fige les dependances runtime (env de reference de la VM, Python 3.14). + +## CI/CD (Partie 4) + +`.forgejo/workflows/pipeline.yml` : declenche sur **pull_request vers `main`**, installe les +dependances puis lance `pipeline.py`. Execute par les runners du **Forgejo de la VM** +(`forgejo.192-168-122-143.nip.io`), mode docker sur `mlops-net` (les donnees `/data/modelling` et la +CA du host sont montees en lecture seule). Secrets (MLflow + S3 Garage) dans les secrets Actions du repo. + +Livrable : `SYNTHESE_TP04.md`. diff --git a/SYNTHESE_TP04.md b/SYNTHESE_TP04.md new file mode 100644 index 0000000..18bbee2 --- /dev/null +++ b/SYNTHESE_TP04.md @@ -0,0 +1,160 @@ +# TP04 - Synthèse : automatisation d'un pipeline ML avec CI/CD + +Fil rouge : prédiction de la consommation électrique (kWh). Aux TP02/TP03, chaque étape (split, +entraînement, `log_model`, enregistrement + promotion au Model Registry) était lancée **à la main**. +Le TP04 **automatise** cet enchaînement en un pipeline reproductible et idempotent (`pipeline.py`), +puis le **déclenche automatiquement via Forgejo Actions** à chaque Pull Request vers `main`. + +## Ce qui a été construit + +- **`pipeline.py`** (racine du dépôt) : orchestre en une seule commande `split -> train --register -> + promote`. Chaque étape est un sous-processus `subprocess.run(..., check=True)` : un échec stoppe le + pipeline avec un code de sortie non nul. Réutilise **telles quelles** les CLI du package `lab/` + (mêmes commandes qu'en manuel) ; ne fait qu'enchaîner et calculer le numéro de version à promouvoir. +- **`requirements.txt`** : dépendances runtime figées sur l'environnement de référence de la VM + (venv `/opt/venvs/mlops`, Python 3.14 ; mlflow 3.13.0, scikit-learn 1.9.0, pandas, pyarrow, boto3, s3fs). +- **`.forgejo/workflows/pipeline.yml`** : workflow CI déclenché sur `pull_request` vers `main`, qui + installe les dépendances puis exécute `pipeline.py`. +- **Repo + CI sur le Forgejo de la VM** (`forgejo.192-168-122-143.nip.io/trainer-admin/ENI-ml-mlops`) : + les 2 runners existants (mode docker, réseau `mlops-net`) exécutent le job. Le code de référence reste + archivé publiquement sur `git.lidge.fr` (comme aux TP02/TP03). + +--- + +## Partie 1 - Analyse de la pipeline existante + +**1.1 - Quelles étapes sont nécessaires pour produire un nouveau modèle ?** +1. **Préparation des données** (`lab/split/cli.py`) : lire le dataset source `/data/modelling/{features, + target}.parquet` et le découper en `train` / `validation` / `test` selon la stratégie de split figée. +2. **Entraînement** (`lab/modeling/cli.py`) : entraîner sur `train`, évaluer sur `validation`, logguer + paramètres et métriques dans MLflow, sauvegarder l'artefact du modèle (`log_model`). +3. **Enregistrement au Registry** (option `--register`) : empiler une **nouvelle version** du modèle + `electricity-consumption`. +4. **Promotion** (`lab/registry/cli.py`) : (re)pointer l'alias `champion` vers cette nouvelle version + (mise à disposition pour l'API de service). + +**1.2 - Quelle étape dépend directement du résultat de la précédente ?** +Le pipeline est une **chaîne linéaire** : `train` dépend de `split` (il lit `train.parquet` et +`validation.parquet` produits par le split) ; l'`enregistrement`/`log_model` dépend de `train` (il +sauvegarde le modèle entraîné) ; la `promotion` dépend de l'enregistrement (elle a besoin du **numéro +de version** qui vient d'être créé). Chaque étape consomme la sortie de la précédente +(parquets -> modèle -> version -> alias). + +**1.3 - Quels artefacts sont produits tout au long du pipeline ?** +- Les jeux de données `data/{train,validation,test}.parquet` (versionnables par DVC) ; +- un **run MLflow** avec ses paramètres (`strategy`, `split_strategy`, `features`) et métriques + (`train/validation_rmse`, `train/validation_mae`, coefficients) ; +- l'**artefact du modèle** sur `s3://mlflow-artifacts` (`model.pkl`, `MLmodel` + signature, + `requirements.txt`/`conda.yaml`/`python_env.yaml`, `input_example`) ; +- une **version** au Model Registry (`electricity-consumption` vN) ; +- l'**alias** `champion` pointant vers cette version. + +--- + +## Partie 2 - Construction d'une pipeline automatisé + +**2.1 - Quels avantages par rapport à une exécution manuelle ?** +Reproductibilité (mêmes étapes, même ordre, mêmes paramètres à chaque exécution), suppression des +erreurs humaines (étape oubliée, mauvais ordre, paramètre incohérent), rapidité (**une seule +commande**), traçabilité, et surtout **exécutabilité par une machine** : le pipeline devient +déclenchable par une plateforme de CI/CD. Le script documente aussi le processus de façon vivante. + +**2.2 - Que se passe-t-il si une étape échoue ?** +Le pipeline **s'arrête immédiatement** : chaque étape est lancée avec `check=True`, donc un échec lève +une exception et le script sort avec un **code non nul**. Les étapes suivantes ne sont pas exécutées +(on n'enregistre pas un modèle à partir d'un split incomplet). En CI, ce code non nul fait **échouer le +job** (signal rouge visible). On corrige la cause, puis on relance. + +**2.3 - Pourquoi est-il important qu'un pipeline soit ré-exécutable sans effets de bord (idempotence) ?** +Pour pouvoir le **rejouer en confiance** (reprise après échec, ré-entraînement périodique, exécution en +CI) sans corrompre l'état ni accumuler d'effets parasites. Ici : le **split est déterministe** (mêmes +dates -> mêmes fichiers, réécrits proprement) ; `--register` **empile une nouvelle version** proprement +(le versioning est le comportement attendu, pas un doublon anarchique) ; l'alias `champion` est +**repointé** (mobile), jamais dupliqué. Rejouer le pipeline redonne donc un état cohérent et prévisible. + +--- + +## Partie 3 - Exécution de la pipeline + +Exécution de référence sur la VM (`set -a; source .env; set +a; python pipeline.py`). + +**3.1 - Quels artefacts ont été produits ?** +- `data/train.parquet` (~115 Mo, 4 485 120 lignes), `validation.parquet` (~48 Mo, 1 855 488), + `test.parquet` (~68 Mo, 2 629 632) ; +- un nouveau run dans l'expérience MLflow `tp02_electricity_consumption` ; +- une **nouvelle version** de `electricity-consumption` (stratégie `full`, `validation_rmse = 6.576`) ; +- l'alias `champion` repointé vers cette nouvelle version. + +**3.2 - Comment vérifier qu'un nouveau modèle a bien été enregistré dans MLflow ?** +- En CLI : `python -m lab.registry.cli versions` (liste les versions, leurs alias et le + `validation_rmse`) ; +- dans l'**UI MLflow** (`https://mlflow.192-168-122-143.nip.io` -> Models -> `electricity-consumption`) : + la nouvelle version apparaît, l'alias `champion` pointe dessus ; +- par l'API : `MlflowClient().search_model_versions(...)` / `get_model_version_by_alias(name, "champion")`. + +**3.3 - Quels éléments garantissent la reproductibilité ?** +- Le **code** versionné (git) et le **workflow** figé ; +- la **configuration** figée dans `lab/constants.py` (`CHOSEN_SPLIT_STRATEGY`, listes de features, + `SERVING_STRATEGY`) et le **split déterministe** par dates ; +- les **données** versionnées par DVC (remote S3 Garage) ; +- les **dépendances épinglées** (`requirements.txt`) + l'environnement capturé par MLflow dans + l'artefact (`requirements.txt`/`conda.yaml`) ; +- le **tracking MLflow** (paramètres, métriques, artefacts) qui permet de retrouver exactement chaque run. + +--- + +## Partie 4 - Automatisation avec Forgejo Actions + +**4.1 - Quel événement déclenche le workflow ?** +Une **Pull Request vers la branche principale** : `on: pull_request: branches: [main]`. L'ouverture +(et chaque mise à jour) d'une PR ciblant `main` lance le workflow. (Démonstration réalisée : PR #1 +`tp04-ci-pipeline -> main`, workflow **vert**, une nouvelle version du modèle enregistrée par la CI.) + +**4.2 - Quel est le rôle de Forgejo Actions dans cette architecture ?** +C'est la **plateforme de CI/CD intégrée au forge**. Elle détecte l'événement (PR), planifie un job et +l'assigne à un **runner**, fournit l'environnement d'exécution (conteneur), **injecte les secrets**, +exécute les étapes (récupération du code, installation des dépendances, exécution de `pipeline.py`) et +**rapporte le statut** (vert/rouge) rattaché à la PR. C'est l'orchestrateur qui automatise l'exécution +du pipeline à chaque changement proposé, sert de garde-fou (une PR qui casse le pipeline est visible +avant le merge) et centralise la traçabilité. + +**4.3 - Différence entre le pipeline Python (partie 2) et le workflow Forgejo Actions ?** +`pipeline.py` porte le **quoi** : la logique métier (l'enchaînement `split -> train -> register -> +promote`), exécutable partout (poste, VM, CI). Le workflow porte le **quand** et le **où** : +l'orchestration CI (quel événement déclenche, sur quel runner, dans quel environnement/conteneur, avec +quels secrets) et le reporting. Le workflow ne contient **aucune logique ML** : il prépare +l'environnement et appelle `pipeline.py`. Cette séparation des responsabilités permet de changer le +déclencheur ou l'infrastructure sans toucher à la logique, et inversement. + +**4.4 - Quels autres événements pourraient déclencher automatiquement le pipeline ?** +Un **push** sur `main` (après merge), la création d'un **tag/release**, une **planification** +(`schedule`/cron, pour un ré-entraînement périodique), un **déclenchement manuel** (`workflow_dispatch`), +ou un **événement externe** (webhook signalant l'arrivée de nouvelles données, appel depuis un autre +workflow). + +--- + +## Détails d'implémentation (Forgejo Actions sur la VM) + +- Le job tourne dans un conteneur `node:20-bookworm` sur le réseau `mlops-net` : il résout les services + par leur nom et joint MLflow/Garage via Caddy en HTTPS (`https://mlflow...`, `https://garage...`), + hôtes autorisés par `MLFLOW_SERVER_ALLOWED_HOSTS`. +- Deux volumes montés en lecture seule : les **données source** `/data/modelling` (le split en a besoin) + et le **magasin de certificats** du host `/etc/ssl/certs/ca-certificates.crt` (confiance envers la CA + interne ENI MLOps ; les runners ont été autorisés à monter ces chemins via `valid_volumes`). +- Les identifiants sont fournis en **secrets Actions** du repo (MLflow basic auth + clés S3 Garage), + jamais committés (`.env` reste gitignoré). Python et les dépendances sont installés dans le job + (`python3-venv` + `pip install -r requirements.txt`). + +## Reproduire + +Sur la VM, `~/tp` : + +```bash +set -a; source .env; set +a +python pipeline.py # split -> train full --register -> promote champion +python -m lab.registry.cli versions # verifier la nouvelle version + alias champion +``` + +En CI : ouvrir une Pull Request vers `main` sur `forgejo.192-168-122-143.nip.io/trainer-admin/ENI-ml-mlops` +-> le workflow `pipeline` s'exécute et enregistre une nouvelle version du modèle (onglet **Actions**).