Compare commits

3 Commits

5 changed files with 325 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
name: pipeline
# Partie 4 : le pipeline ML est rejoue automatiquement a chaque Pull Request
# vers la branche principale (Q4.1). Autres declencheurs possibles (Q4.4) :
# push sur main, tag de release, planification (schedule/cron), declenchement
# manuel (workflow_dispatch).
on:
pull_request:
branches: [main]
jobs:
train-and-register:
runs-on: docker
# Le runner (mode docker, reseau mlops-net) lance le job dans ce conteneur.
# On y monte en lecture seule les donnees source du fil rouge et le magasin
# de certificats du host (pour faire confiance a la CA interne ENI MLOps).
container:
image: node:20-bookworm
volumes:
- /data/modelling:/data/modelling:ro
- /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro
steps:
- name: Recuperation du code
uses: actions/checkout@v4
- name: Installation de Python et des dependances
run: |
apt-get update
apt-get install -y --no-install-recommends python3-venv
python3 -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install -r requirements.txt
- name: Execution du pipeline (split -> train --register -> promote)
run: .venv/bin/python pipeline.py
env:
# Serveur MLflow (basic auth) : URLs HTTPS via Caddy, resolues sur mlops-net.
MLFLOW_TRACKING_URI: ${{ secrets.MLFLOW_TRACKING_URI }}
MLFLOW_TRACKING_USERNAME: ${{ secrets.MLFLOW_TRACKING_USERNAME }}
MLFLOW_TRACKING_PASSWORD: ${{ secrets.MLFLOW_TRACKING_PASSWORD }}
MLFLOW_EXPERIMENT_NAME: ${{ secrets.MLFLOW_EXPERIMENT_NAME }}
# Artefacts sur S3 Garage (log_model).
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
MLFLOW_S3_ENDPOINT_URL: ${{ secrets.MLFLOW_S3_ENDPOINT_URL }}
# CA interne ENI MLOps (requests/boto3 n'utilisent pas le store systeme par defaut).
REQUESTS_CA_BUNDLE: /etc/ssl/certs/ca-certificates.crt
AWS_CA_BUNDLE: /etc/ssl/certs/ca-certificates.crt
# Import du package lab depuis la racine du depot.
PYTHONPATH: ${{ github.workspace }}

View File

@@ -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`.

160
SYNTHESE_TP04.md Normal file
View File

@@ -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**).

70
pipeline.py Normal file
View File

@@ -0,0 +1,70 @@
"""Pipeline ML automatise : split -> entrainement (--register) -> promotion.
Orchestre en une seule commande (`python pipeline.py`) les etapes du fil rouge
menant a la mise a disposition du modele dans le MLflow Model Registry :
1. split : (re)cree data/{train,validation,test}.parquet depuis /data/modelling
(strategie de split figee dans lab/constants.py) ;
2. train : entraine la strategie de service (`full`) et enregistre une
nouvelle version du modele dans le Registry (log_model + --register) ;
3. promote : (re)pointe l'alias `champion` vers cette nouvelle version.
Chaque etape est un sous-processus lance avec `check=True` : si une etape echoue,
le pipeline s'arrete immediatement avec un code de sortie non nul et n'execute
aucune etape suivante (Partie 2, Q2.2). Le pipeline est reexecutable sans effet
de bord indesirable (idempotence, Q2.3) : le split est deterministe, `--register`
empile proprement une nouvelle version, et l'alias `champion` est repointe
(jamais duplique).
Les etapes reutilisent telles quelles les CLI du package `lab/` (memes commandes
qu'une execution manuelle des TP precedents) : le pipeline ne fait que les
enchainer et calculer le numero de la version a promouvoir.
"""
import logging
import subprocess
import sys
from mlflow import MlflowClient
from lab import constants
logging.basicConfig(level=logging.INFO, format="%(asctime)s [pipeline] %(message)s")
logger = logging.getLogger("pipeline")
def run_step(title: str, module_args: list[str]) -> None:
"""Execute une etape (module `lab`) dans un sous-processus ; stoppe si elle echoue."""
logger.info("=== ETAPE %s : python -m %s", title, " ".join(module_args))
subprocess.run([sys.executable, "-m", *module_args], check=True)
def latest_model_version(name: str) -> int:
"""Numero de la derniere version enregistree du modele (celle qui vient d'etre creee)."""
client = MlflowClient()
versions = client.search_model_versions(f"name = '{name}'")
if not versions:
raise RuntimeError(f"Aucune version enregistree pour le modele '{name}'.")
return max(int(mv.version) for mv in versions)
def main() -> None:
strategy = constants.SERVING_STRATEGY
name = constants.REGISTERED_MODEL_NAME
alias = constants.MODEL_ALIAS
# 1. Preparation des jeux de donnees (deterministe).
run_step("split", ["lab.split.cli"])
# 2. Entrainement + enregistrement d'une nouvelle version dans le Model Registry.
run_step("train", ["lab.modeling.cli", strategy.value, "--register"])
# 3. Promotion : repointer l'alias vers la version qui vient d'etre enregistree.
version = latest_model_version(name)
run_step("promote", ["lab.registry.cli", "promote", "--version", str(version), "--alias", alias])
logger.info("Pipeline termine avec succes : %s v%s -> alias '%s'", name, version, alias)
if __name__ == "__main__":
main()

14
requirements.txt Normal file
View File

@@ -0,0 +1,14 @@
# Dependances runtime du pipeline (Partie 4 : "installer les dependances").
# Versions figees sur l'environnement de reference de la VM (venv /opt/venvs/mlops,
# Python 3.14) pour garantir la reproductibilite et la coherence avec le serveur
# MLflow (3.13.0) et l'API de service (chargement du modele enregistre).
mlflow==3.13.0
scikit-learn==1.9.0
scipy==1.17.1
numpy==2.4.6
pandas==2.3.3
pyarrow==24.0.0
typer==0.26.7
boto3==1.43.0
botocore==1.43.0
s3fs==2026.6.0