diff --git a/.forgejo/workflows/pipeline.yml b/.forgejo/workflows/pipeline.yml new file mode 100644 index 0000000..9d03f60 --- /dev/null +++ b/.forgejo/workflows/pipeline.yml @@ -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 + uses: actions/setup-python@v5 + with: + python-version: "3.14" + + - name: Installation des dependances + run: pip install -r requirements.txt + + - name: Execution du pipeline (split -> train --register -> promote) + run: 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 }} diff --git a/pipeline.py b/pipeline.py new file mode 100644 index 0000000..5fee2c4 --- /dev/null +++ b/pipeline.py @@ -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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8d64e5c --- /dev/null +++ b/requirements.txt @@ -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