Module 7 — Chaînes d'intégration et de livraison continues
L'équipe d'Alice fait maintenant 12 commits par semaine sur le dépôt du
modèle. Sans chaîne d'intégration, chaque commit est une invitation à
casser silencieusement le pipeline. Ce module met en place les tests qui
comptent pour un projet d'apprentissage, puis les assemble dans une
chaîne GitHub Actions concrète.
Ce qu'il faut tester dans un projet MLOps
Les tests unitaires classiques (pytest) restent utiles : chaque fonction
de transformation de variables, chaque adaptateur de base doit avoir son
test. Mais deux familles de tests spécifiques à l'apprentissage
distinguent une chaîne MLOps d'une chaîne logicielle ordinaire.
Les tests de données. Ils protègent le pipeline de l'arrivée d'un jeu qui ne ressemble plus à ce qu'on attend :
import pandera as pa
from pandera.typing import Series
class SchemaAbonne(pa.DataFrameModel):
id_abonne: Series[str] = pa.Field(unique=True)
age: Series[int] = pa.Field(ge=18, le=110)
anciennete_mois: Series[int] = pa.Field(ge=0, le=600)
forfait: Series[str] = pa.Field(isin=["A", "B", "C", "D"])
taux_utilisation: Series[float] = pa.Field(ge=0.0, le=1.0)
Ce schéma refuse un âge de 250, un forfait inconnu, un identifiant en double, un taux d'utilisation à 3,7. Ces valeurs sont rares mais elles tombent, et sans schéma elles empoisonnent l'entraînement en silence.
Les tests de modèle avec seuils. Ils protègent la production d'une régression sournoise :
def test_auc_sur_validation_figee():
modele = charger("candidat")
x, y = charger_validation_figee()
auc = roc_auc_score(y, modele.predict_proba(x)[:, 1])
assert auc >= 0.80, f"AUC {auc:.3f} sous le seuil de 0.80"
def test_pas_de_regression_par_tranche():
modele = charger("candidat")
for segment in ["pro", "prepaye", "nouveau"]:
auc = auc_sur_segment(modele, segment)
assert auc >= AUC_CHAMPION[segment] - 0.02, (
f"regression de {AUC_CHAMPION[segment] - auc:.3f} sur {segment}"
)
Ces tests ne remplacent pas l'expérimentation : ils garantissent qu'aucun commit ne dégrade tacitement le modèle en dessous de seuils sur lesquels l'équipe s'est engagée.
Une chaîne GitHub Actions concrète
Un fichier .github/workflows/mlops.yml orchestre les étapes :
name: mlops
on:
push:
branches: [main]
pull_request:
jobs:
qualite:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- run: uv sync --frozen
- run: uv run ruff check .
- run: uv run pytest tests/unitaires -q
donnees_et_modele:
needs: qualite
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: iterative/setup-dvc@v1
- run: dvc pull data/validation_figee.parquet
- run: uv run pytest tests/donnees -q
- run: uv run pytest tests/modele -q
image:
needs: donnees_et_modele
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/mon-org/churn:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- run: trivy image --exit-code 1 --severity HIGH,CRITICAL ghcr.io/mon-org/churn:${{ github.sha }}
deployer_preprod:
needs: image
runs-on: ubuntu-latest
environment: preproduction
steps:
- run: ./deploiement/appliquer.sh preprod ${{ github.sha }}
deployer_prod:
needs: deployer_preprod
runs-on: ubuntu-latest
environment:
name: production
url: https://churn.exemple.com
steps:
- run: ./deploiement/appliquer.sh prod ${{ github.sha }}
L'ordre importe : la qualité (linting, tests unitaires) est peu coûteuse
et attrape 80 % des erreurs ; on ne fait pas tourner les tests de modèle
si le linting est cassé. La construction d'image n'a lieu que sur main
et déclenche un scan trivy qui échoue en cas de faille critique.
L'approbation manuelle qui protège la production
Le déclencheur environment: production de GitHub Actions porte une
règle : « ce job ne démarre qu'après approbation d'une personne
autorisée ». Cette pause obligée n'est pas un frein bureaucratique ; c'est
la barrière qui empêche qu'une chaîne verte pousse en production un
modèle dont personne n'a réellement lu la fiche.
L'approbateur consulte :
- Le résumé des métriques de la nouvelle version dans
MLflow. - La comparaison par tranches vs. la version
championactuelle. - Le journal du scan de sécurité.
- La note de version rédigée par l'auteur du commit.
S'il approuve, l'alias champion bascule automatiquement (module 5) et
le service redémarre.
Cache, parallélisation et coût
Trois techniques rendent la chaîne rapide sans la dénaturer :
- Cache du gestionnaire de paquets (
actions/setup-pythonaveccache: pipousetup-uv). Passe l'installation de 2 minutes à 15 secondes. - Cache Docker Buildx (
cache-from: type=gha). Une image dont seule la couche du modèle change se reconstruit en 20 secondes. - Jobs parallèles quand ils sont indépendants (
qualiteetdonnees_et_modelepeuvent s'exécuter en parallèle).
Une chaîne qui prend une heure sera contournée par l'équipe (« je rebasculerai plus tard »). Une chaîne qui prend cinq minutes devient une seconde nature.
Un test de modèle est stochastique : il compare une métrique à un
seuil, il ne compare pas une sortie à une valeur attendue. Il faut donc
fixer les graines (module 2) pour qu'un même commit donne toujours le
même verdict, et laisser une marge (AUC_CHAMPION - 0.02) pour absorber
le bruit résiduel. Un seuil trop serré crée une chaîne rouge intermittente
que l'équipe apprend à ignorer.
En résumé
- Testez les données avec un schéma (
pandera), le modèle avec des seuils par tranche, et gardez les tests unitaires classiques du code. - Structurez la chaîne en jobs dépendants : qualité, données et modèle, image scannée, préproduction, production.
- Exigez une approbation humaine avant la production ; elle protège contre les cas que le code ne peut pas juger.
- Cachez ce qui peut l'être ; une chaîne rapide est une chaîne suivie.
Le module 8 choisit entre trois manières de servir le modèle : par lots, en ligne ou en flux, selon la latence exigée par le cas d'usage.