Dotika MLOps · Bloc 7 — L'échelleBlock 7 — Scale
▶ Slides▶ Slides ← Sommaire← Contents
Bloc 7 · 12 h · théorie 2h30 · lab 9h30Block 7 · 12 h · theory 2h30 · lab 9h30

L'échelleScale

Le système est testé et automatisé — mais il vit dans un seul conteneur, sur une seule machine. Trois jours pour le faire tourner en prod : répliqué sur un cluster, mis à jour sans coupure, et surveillé jusqu'au drift du modèle. The system is tested and automated — but it lives in one container, on one machine. Three days to run it in production: replicated on a cluster, updated with zero downtime, and watched all the way to model drift.

Où on en estWhere we stand

Une chaîne parfaite… vers une machine uniqueA perfect pipeline… into a single machine

Bloc 6 : chaque commit est testé, validé, versionné. Mais au bout de la chaîne, un seul conteneur fait tout le travail. Qu'il meure — panne, pic de trafic, mise à jour — et plus aucune fraude n'est détectée. Block 6: every commit is tested, validated, versioned. But at the end of the line, a single container does all the work. If it dies — failure, traffic spike, update — no fraud gets detected anymore.

Aujourd'huiToday

Le cluster : des réplicas derrière un aiguilleurThe cluster: replicas behind a balancer

Regardez la carte : la boîte s'est multipliée. Un load balancer répartit le trafic sur des réplicas ; un pod meurt, un autre renaît — personne ne s'en aperçoit. C'est l'idée déclarative : on décrit l'état désiré, le cluster corrige l'écart. Look at the map: the box has multiplied. A load balancer spreads traffic across replicas; one pod dies, another is reborn — nobody notices. That's the declarative idea: describe the desired state, the cluster closes the gap.

Et surtout…And above all…

Les capteurs s'allument — jusqu'au driftThe sensors light up — down to drift

Le monitoring veille : latence et erreurs côté infra, distribution des scores et drift des features côté modèle. Quand le monde change trop, l'alerte part — et la flèche du réentraînement referme la boucle du MLOps. Monitoring is on watch: latency and errors on the infra side, score distribution and feature drift on the model side. When the world shifts too far, the alert fires — and the retraining arrow closes the MLOps loop.

Actes 1-2 · pourquoi l'orchestration, et le vocabulaireActs 1-2 · why orchestration, and the vocabulary

Un conteneur seul ne suffit pasA single container is not enough

2 h du matin, Black Friday, trafic ×50, un serveur meurt — et personne n'est réveillé : le cluster a réparé seul. Le secret n'est pas la magie, c'est l'idée déclarative : on ne donne pas des ordres, on décrit un état désiré — « je veux 3 réplicas en bonne santé » — et le chef d'orchestre corrige l'écart, en continu. 2 a.m., Black Friday, traffic ×50, a server dies — and nobody gets woken up: the cluster healed itself. The secret isn't magic, it's the declarative idea: you don't give orders, you describe a desired state — "I want 3 healthy replicas" — and the conductor closes the gap, continuously.

PROBLÈME 1PROBLEM 1
La panneFailure
Le conteneur meurt à 2 h du matin : qui le relance ? La machine meurt : qui le déplace ? Personne — donc coupure.The container dies at 2 a.m.: who restarts it? The machine dies: who moves it? Nobody — so, downtime.
PROBLÈME 2PROBLEM 2
L'échelleScale
Un conteneur = une capacité fixe. Le Black Friday en demande dix — puis trois à 4 h du matin.One container = fixed capacity. Black Friday needs ten — then three at 4 a.m.
PROBLÈME 3PROBLEM 3
La mise à jourUpdates
Remplacer v12 par v13 sans couper ? Avec un seul conteneur, stop = coupure — et un détecteur coupé, c'est la porte ouverte.Swap v12 for v13 without cutting service? With one container, stop = outage — and a detector that's down is an open door.
Le pod — la plus petite unitéThe pod — the smallest unit
Votre conteneur habillé pour le cluster : une IP, des ressources, un cycle de vie. Éphémère par conception — ne vous attachez pas.Your container dressed for the cluster: an IP, resources, a life cycle. Ephemeral by design — don't get attached.
Le deployment — le contratThe deployment — the contract
« 3 réplicas de la v12, en bonne santé, en permanence. » Un pod meurt, il le recrée ; l'image change, il orchestre le remplacement."3 replicas of v12, healthy, at all times." A pod dies, it recreates it; the image changes, it orchestrates the swap.
Le service — le standardThe service — the switchboard
Les pods changent d'adresse sans cesse. Le service est le numéro fixe qui route vers les pods vivants du moment. On n'appelle jamais un pod.Pods change addresses constantly. The service is the fixed number routing to whichever pods are alive. You never call a pod.
ConfigMap & Secret
La config hors de l'image : seuils et URLs en ConfigMap, credentials en Secret. Une seule image, portée par des configs par environnement.Config outside the image: thresholds and URLs in a ConfigMap, credentials in a Secret. One image, carried by per-environment configs.
À retenirTakeaway

Tout s'écrit en manifests YAML versionnés dans Git — l'infrastructure se relit et se revoit comme du code. Et cinq commandes kubectl suffisent : apply, get, describe, logs, rollout undo. Déclarer, constater, enquêter, lire, reculer. Everything is written as YAML manifests versioned in Git — infrastructure gets read and reviewed like code. And five kubectl commands are enough: apply, get, describe, logs, rollout undo. Declare, observe, investigate, read, step back.

Acte 3 · déployer le détecteurAct 3 · deploying the detector

Le pod zombie et les trois sondesThe zombie pod and the three probes

Le process tourne, le port répond… et l'application est morte. Kubernetes ne peut pas le deviner — sauf si on lui donne des yeux. Les probes sont trois questions posées en boucle à chaque pod : The process runs, the port answers… and the application is dead. Kubernetes cannot guess it — unless you give it eyes. Probes are three questions asked of every pod, on a loop:

Liveness · « es-tu vivant ? »"are you alive?"
Échecs répétés → le pod est tué et remplacé. Le remède au zombie : plutôt renaître que ramper.Repeated failures → the pod is killed and replaced. The cure for the zombie: better reborn than crawling.
Readiness · « es-tu prêt ? »"are you ready?"
Échec → le service cesse de lui envoyer du trafic, sans le tuer. Surchargé ou en train de charger ? On le laisse souffler.Failure → the service stops sending it traffic, without killing it. Overloaded or still loading? Let it breathe.
Startup · « as-tu fini de démarrer ? »"done booting?"
Tant qu'elle n'a pas réussi, les deux autres attendent. La période de grâce du démarrage.Until it succeeds, the other two wait. The grace period of startup.
Le détail spécifique MLThe ML-specific detail

Nos modèles sont longs à charger : des centaines de Mo en RAM, 30 à 90 secondes. Sans startup probe, la liveness tue le pod avant la fin du chargement — redémarrage, rechargement, re-mise à mort : la boucle de crash infinie, le grand classique du déploiement ML. Une startup probe généreuse + un /ready qui ne dit OK que si le modèle est en mémoire, et le problème disparaît. Our models are slow to load: hundreds of MB into RAM, 30 to 90 seconds. Without a startup probe, liveness kills the pod before loading finishes — restart, reload, killed again: the infinite crash loop, the great classic of ML deployments. A generous startup probe + a /ready that only says OK once the model is in memory, and the problem vanishes.

Acte 4 · livrer sans couperAct 4 · shipping without downtime

Trois stratégies, et un élastiqueThree strategies, and an elastic

La v13 est prête. Comment la mettre en face de vrais clients sans couper le service — et sans parier toute la prod sur elle ? v13 is ready. How do you put it in front of real customers without cutting the service — and without betting all of production on it?

ROLLING
Remplacer un à un — la routineReplace one by one — the routine
Un pod v13 démarre, passe ready, un v12 s'éteint — et on répète. Jamais sous l'effectif. Le défaut de Kubernetes.One v13 pod starts, turns ready, one v12 shuts down — repeat. Never below strength. Kubernetes' default.
CANARY
5 % d'abord — la reine pour les modèles5 % first — the queen for models
La v13 goûte 5 % du trafic réel. Les métriques tiennent — y compris celles du modèle ? 25, 50, 100 %. Sinon : retour à 0, personne n'a rien vu.v13 tastes 5 % of real traffic. Metrics hold — including the model's? 25, 50, 100 %. Otherwise: back to 0, nobody saw a thing.
BLUE-GREEN
Deux mondes, un aiguillageTwo worlds, one switch
La v13 complète à côté de la v12, bascule et retour instantanés — au prix de deux environnements qui tournent en même temps.A complete v13 next to v12, instant switch and return — at the cost of two environments running at once.
HPA
Le scaling automatiqueAutomatic scaling
Le HorizontalPodAutoscaler observe CPU ou requêtes/s et ajuste les réplicas entre min et max : 3 → 10 au pic, retour à 3 la nuit.The HorizontalPodAutoscaler watches CPU or requests/s and adjusts replicas between min and max: 3 → 10 at the peak, back to 3 at night.
À retenirTakeaway

Pour un nouveau modèle, le verdict est sans appel : canary. Un modèle peut passer tous les tests offline et échouer en prod — la seule vérité est le trafic réel, et on préfère qu'il le rencontre à 5 %. For a new model, the verdict is final: canary. A model can pass every offline test and fail in production — the only truth is real traffic, and we'd rather it meet it at 5 %.

Acte 5 · les yeuxAct 5 · the eyes

Monitorer le modèle, pas seulement l'APIMonitor the model, not just the API

Prometheus collecte, Grafana montre : latence, erreurs, saturation — la santé de la machine. Mais votre API peut être parfaitement verte et votre modèle parfaitement faux : 200 OK, 40 ms… et un détecteur qui laisse tout passer. C'est la partie que 90 % des équipes oublient. Prometheus collects, Grafana shows: latency, errors, saturation — the machine's health. But your API can be perfectly green and your model perfectly wrong: 200 OK, 40 ms… and a detector that lets everything through. It's the part 90 % of teams forget.

3
capteurs côté modèle : distribution des scores, taux d'alerte, drift des featuresmodel-side sensors: score distribution, alert rate, feature drift
15 s
entre deux passages de Prometheus sur /metrics — infra et modèle, même tuyaubetween two Prometheus scrapes of /metrics — infra and model, same pipe
90 %
des équipes ne monitorent que l'infra — et découvrent le drift des mois trop tardof teams only monitor infra — and discover drift months too late
CAPTEUR 1SENSOR 1
La distribution des scoresThe score distribution
Hier centrée sur 0,1 — aujourd'hui sur 0,4 ? Quelque chose a changé. Pas dans le code : dans le monde.Centered on 0.1 yesterday — on 0.4 today? Something changed. Not in the code: in the world.
CAPTEUR 2SENSOR 2
Le taux d'alerteThe alert rate
0,2 % en temps normal ; 0 % ou 3 % ce matin : le modèle dort, ou crie au loup. Les deux coûtent cher.0.2 % on a normal day; 0 % or 3 % this morning: the model is asleep, or crying wolf. Both are expensive.
CAPTEUR 3SENSOR 3
Le drift des featuresFeature drift
On compare les distributions de prod à celles de l'entraînement, feature par feature. L'écart franchit le seuil → alerte. Le signal le plus précoce.Compare production distributions to training ones, feature by feature. The gap crosses the threshold → alert. The earliest signal of all.
LA BOUCLETHE LOOP
L'alerte qui referme le cycleThe alert that closes the cycle
Drift → alerte → réentraînement → v14 au registry → canary → prod. Le cycle de vie du bloc 1 devient un système vivant.Drift → alert → retraining → v14 in the registry → canary → prod. Block 1's life cycle becomes a living system.
À retenirTakeaway

Le modèle ne lève jamais d'exception quand il se trompe : il faut instrumenter ses opinions. Et quand l'alerte de drift déclenche le réentraînement, la boucle du MLOps se ferme — c'est exactement pour ça qu'on a construit tout le reste. The model never raises an exception when it's wrong: you must instrument its opinions. And when the drift alert triggers retraining, the MLOps loop closes — which is exactly why we built everything else.

Acte 6 · quand ça tourne malAct 6 · when things go wrong

La marche arrière, et les trois signauxReverse gear, and the three signals

Un incident ML a toujours deux suspects : le code et le modèle. La marche arrière se prépare — et se répète — aux deux niveaux, à froid. An ML incident always has two suspects: the code and the model. Reverse gear is prepared — and rehearsed — on both levels, in calm weather.

Rollback à deux niveauxTwo-level rollback
Côté app : kubectl rollout undo — la version précédente revient en secondes. Côté modèle : repointer l'alias Production du registry MLflow, v13 → v12. Un geste chacun.App side: kubectl rollout undo — the previous version is back in seconds. Model side: repoint the MLflow registry's Production alias, v13 → v12. One move each.
Observabilité end-to-endEnd-to-end observability
Logs (ce qui s'est dit), métriques (combien), traces (où le temps est passé). Avec un identifiant de corrélation, on suit UNE transaction de l'API au verdict — « pourquoi celle-ci ? » se répond en minutes.Logs (what was said), metrics (how much), traces (where the time went). With a correlation id, you follow ONE transaction from API to verdict — "why this one?" gets answered in minutes.
Post-mortem sans blâmeBlameless post-mortem
On ne cherche pas qui a cassé — on cherche ce qui a laissé la casse se produire. Chronologie, impact, causes, actions : court, à froid, versionné avec le code.You don't look for who broke it — you look for what allowed the breakage. Timeline, impact, causes, actions: short, written calm, versioned with the code.
La mission du blocThe block's mission

« Le détecteur tourne répliqué sur un cluster : il encaisse les pannes et la charge, se met à jour sans coupure — et prévient dès qu'il dérive. » "The detector runs replicated on a cluster: it absorbs failures and load, updates with zero downtime — and warns the moment it drifts."

Le système cesse d'être une démo sur un laptop et devient un service — celui qu'on assemblera et présentera de bout en bout au bloc 8. The system stops being a laptop demo and becomes a service — the one we'll assemble and present end to end in block 8.

Lab · 9h30Lab · 9h30

À vous de jouerYour turn

Cinq missions sur trois jours — 1 h de concepts le jour 3, la grosse journée de 8 h le jour 4, et 3 h le jour 5 pour le rollback et l'observabilité. Les durées viennent du plan de cours : Five missions across three days — 1 h of concepts on day 3, the big 8-hour day on day 4, and 3 h on day 5 for rollback and observability. Durations come straight from the course plan:

1h30
Manifests & concepts — cluster kind, deployment + service + configmap, et le pod tué qui renaît tout seulManifests & concepts — kind cluster, deployment + service + configmap, and the killed pod reborn on its own
pratiquehands-on
2h00
Déployer + probes — votre API (image du registry) avec liveness / readiness / startup réglées pour le chargement du modèleDeploy + probes — your API (registry image) with liveness / readiness / startup tuned for model loading
pratiquehands-on
2h00
Stratégies + HPA — rolling sous trafic, canary manuel 5 % → 100 %, HPA observé en montée et en descenteStrategies + HPA — rolling under traffic, manual canary 5 % → 100 %, HPA observed scaling up and down
pratiquehands-on
2h30
Monitoring + drift — Prometheus + Grafana, métriques du modèle exposées, script de drift et alerte qui part vraimentMonitoring + drift — Prometheus + Grafana, model metrics exposed, drift script and an alert that really fires
pratiquehands-on
1h30
Rollback + observabilité — v13 cassée en canary, retour aux deux niveaux, transaction suivie par son id, post-mortem d'une pageRollback + observability — broken v13 as canary, rollback on both levels, transaction followed by its id, one-page post-mortem
pratiquehands-on
Definition of done

L'API servie par le cluster (un kubectl delete pod en pleine charge sans requête perdue), un canary mené 5 % → 100 % et son retour maîtrisé, le HPA vu en action, un dashboard à deux étages — infra et modèle — avec une alerte de drift qui part, et un post-mortem d'une page. Il ne reste qu'à tout assembler et le prouver : bloc 8. The API served by the cluster (a kubectl delete pod under full load without a lost request), a canary run 5 % → 100 % with a controlled return, the HPA seen in action, a two-storey dashboard — infra and model — with a drift alert that fires, and a one-page post-mortem. All that's left is to assemble it all and prove it: block 8.

Pas à pas · en autonomieStep by step · self-paced

Faites-le vous-même, de A à ZDo it yourself, start to finish

Ce guide reprend les cinq missions du lab, commande par commande, pour tout refaire seul. Prérequis : les blocs 4 à 6 terminés — Docker en marche, l'image fraud-api:0.1 construite au bloc 4, le serveur MLflow du bloc 5 lancé (mlflow server --host 0.0.0.0 --port 5000, alias @production posé sur fraud-detector), et le dépôt fraud-detection/. À la fin : l'API répliquée sur un cluster kind, mise à jour sans coupure, autoscalée, surveillée par Prometheus — et un script de drift qui crie quand le monde change. This guide replays the lab's five missions, command by command, so you can redo everything on your own. Prerequisites: blocks 4 to 6 finished — Docker running, the fraud-api:0.1 image built in block 4, block 5's MLflow server up (mlflow server --host 0.0.0.0 --port 5000, the @production alias set on fraud-detector), and the fraud-detection/ repo. At the end: the API replicated on a kind cluster, updated with zero downtime, autoscaled, watched by Prometheus — and a drift script that shouts when the world changes.

Mission 1 — Manifests & concepts (1h30)Mission 1 — Manifests & concepts (1h30)

1Installer kind et kubectlInstall kind and kubectl

kind (« Kubernetes in Docker ») fait tourner un vrai cluster dans des conteneurs Docker — parfait pour apprendre sans facture cloud. kubectl est la télécommande universelle de n'importe quel cluster Kubernetes. kind ("Kubernetes in Docker") runs a real cluster inside Docker containers — perfect for learning without a cloud bill. kubectl is the universal remote for any Kubernetes cluster.

# macOS
brew install kind kubectl

# Linux
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.23.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
curl -LO https://dl.k8s.io/release/v1.30.0/bin/linux/amd64/kubectl
chmod +x kubectl
sudo mv kubectl /usr/local/bin/kubectl

kind version
kubectl version --client
Vous devriez voir : kind v0.23.x (ou plus récent) et la version client de kubectl, sans erreur.You should see: kind v0.23.x (or newer) and kubectl's client version, with no error.
2Créer le cluster localCreate the local cluster

Une seule commande crée un nœud Kubernetes complet dans un conteneur et configure kubectl pour lui parler. One single command creates a complete Kubernetes node inside a container and configures kubectl to talk to it.

kind create cluster --name mlops
kubectl get nodes
Vous devriez voir : un nœud mlops-control-plane en STATUS Ready (comptez 60 à 90 secondes au premier lancement).You should see: one mlops-control-plane node with STATUS Ready (allow 60 to 90 seconds on the first run).
3Charger l'image locale dans le clusterLoad the local image into the cluster

Le cluster kind ne voit pas les images de votre Docker local : il faut les charger explicitement dans ses nœuds. L'oublier est LA cause n° 1 du fameux ImagePullBackOff. The kind cluster cannot see your local Docker images: you must load them into its nodes explicitly. Forgetting this is THE number 1 cause of the infamous ImagePullBackOff.

docker images fraud-api
kind load docker-image fraud-api:0.1 --name mlops
Vous devriez voir : l'image fraud-api:0.1 listée par docker images, puis la ligne « Image: "fraud-api:0.1" … loaded » sans erreur.You should see: the fraud-api:0.1 image listed by docker images, then the line "Image: "fraud-api:0.1" … loaded" with no error.

Mission 2 — Déployer + probes (2h00)Mission 2 — Deploy + probes (2h00)

4k8s/configmap.yaml — la config hors de l'imagek8s/configmap.yaml — config outside the image

L'URL de MLflow ne vit pas dans l'image : elle vit dans une ConfigMap. Depuis un pod kind, la machine hôte s'appelle host.docker.internal (macOS / Windows) ; sur Linux, remplacez par http://172.17.0.1:5000 (l'IP du pont docker0). The MLflow URL does not live in the image: it lives in a ConfigMap. From a kind pod, the host machine is called host.docker.internal (macOS / Windows); on Linux, replace it with http://172.17.0.1:5000 (the docker0 bridge IP).

cd fraud-detection
mkdir -p k8s
cat > k8s/configmap.yaml <<'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
  name: fraud-api-config
data:
  MLFLOW_TRACKING_URI: "http://host.docker.internal:5000"
EOF
kubectl apply -f k8s/configmap.yaml
Vous devriez voir : configmap/fraud-api-config created.You should see: configmap/fraud-api-config created.
5k8s/deployment.yaml — 3 réplicas et les trois sondesk8s/deployment.yaml — 3 replicas and the three probes

Le contrat complet : 3 réplicas, ressources bornées, et les trois probes. La startupProbe est généreuse (30 × 2 s = 60 s de grâce) parce que le modèle est long à charger — sans elle, la liveness tue le pod avant la fin du chargement, en boucle. Gardez le serveur MLflow du bloc 5 allumé : les pods chargent le modèle au démarrage. The full contract: 3 replicas, bounded resources, and the three probes. The startupProbe is generous (30 × 2 s = 60 s of grace) because the model is slow to load — without it, liveness kills the pod before loading finishes, in a loop. Keep block 5's MLflow server on: pods load the model at startup.

cat > k8s/deployment.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fraud-api
  labels:
    app: fraud-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: fraud-api
      track: stable
  template:
    metadata:
      labels:
        app: fraud-api
        track: stable
    spec:
      containers:
        - name: fraud-api
          image: fraud-api:0.1
          imagePullPolicy: Never
          ports:
            - containerPort: 8000
          env:
            - name: MLFLOW_TRACKING_URI
              valueFrom:
                configMapKeyRef:
                  name: fraud-api-config
                  key: MLFLOW_TRACKING_URI
          resources:
            requests:
              cpu: 250m
              memory: 512Mi
            limits:
              cpu: 500m
              memory: 1Gi
          startupProbe:
            httpGet:
              path: /health
              port: 8000
            failureThreshold: 30
            periodSeconds: 2
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            periodSeconds: 5
            failureThreshold: 3
          livenessProbe:
            httpGet:
              path: /health
              port: 8000
            periodSeconds: 10
            failureThreshold: 3
EOF
kubectl apply -f k8s/deployment.yaml
Vous devriez voir : deployment.apps/fraud-api created.You should see: deployment.apps/fraud-api created.
6k8s/service.yaml — le standard, puis le premier appelk8s/service.yaml — the switchboard, then the first call

Le Service sélectionne app: fraud-api sans regarder track — ce détail servira au canary. On observe les pods démarrer, puis on appelle l'API servie par le cluster. The Service selects app: fraud-api without looking at track — that detail will matter for the canary. Watch the pods start, then call the API served by the cluster.

cat > k8s/service.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: fraud-api
spec:
  type: ClusterIP
  selector:
    app: fraud-api
  ports:
    - port: 80
      targetPort: 8000
EOF
kubectl apply -f k8s/service.yaml
kubectl get pods -w
# Ctrl-C once the 3 pods are Running and READY 1/1, then:
kubectl port-forward service/fraud-api 8000:80 &
sleep 2
curl -s http://localhost:8000/health
Vous devriez voir : les 3 pods passer de ContainerCreating à Running 1/1, puis le JSON de /health avec "status":"ok" — servi depuis le cluster.You should see: the 3 pods go from ContainerCreating to Running 1/1, then the /health JSON with "status":"ok" — served from the cluster.
7Le self-healing, de vos propres yeuxSelf-healing, with your own eyes

Tuez un pod : le Deployment constate l'écart avec l'état désiré (3 réplicas) et en recrée un, sans vous demander la permission. C'est l'idée déclarative en action. Kill a pod: the Deployment notices the gap with the desired state (3 replicas) and recreates one, without asking your permission. The declarative idea, in action.

kubectl get pods
kubectl delete pod $(kubectl get pods -l app=fraud-api -o jsonpath='{.items[0].metadata.name}')
kubectl get pods
Vous devriez voir : le pod supprimé disparaît et un remplaçant avec un AGE de quelques secondes apparaît — toujours 3 réplicas.You should see: the deleted pod vanishes and a replacement with an AGE of a few seconds appears — still 3 replicas.

Mission 3 — Stratégies + HPA (2h00)Mission 3 — Strategies + HPA (2h00)

8Le canary : 1 réplica de la v0.2 face à 3 stablesThe canary: 1 replica of v0.2 next to 3 stable ones

Construisez la v0.2 (le contenu peut être identique — c'est le mécanisme qu'on observe), chargez-la dans kind, puis déployez-la en canary : mêmes labels app=fraud-api, mais track=canary. Le Service route vers les 4 pods → le canary reçoit ~25 % du trafic. Build v0.2 (the content can be identical — the mechanism is what we observe), load it into kind, then deploy it as a canary: same app=fraud-api labels, but track=canary. The Service routes to all 4 pods → the canary receives ~25% of the traffic.

docker build -t fraud-api:0.2 .
kind load docker-image fraud-api:0.2 --name mlops
cat > k8s/deployment-canary.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fraud-api-canary
  labels:
    app: fraud-api
spec:
  replicas: 1
  selector:
    matchLabels:
      app: fraud-api
      track: canary
  template:
    metadata:
      labels:
        app: fraud-api
        track: canary
    spec:
      containers:
        - name: fraud-api
          image: fraud-api:0.2
          imagePullPolicy: Never
          ports:
            - containerPort: 8000
          env:
            - name: MLFLOW_TRACKING_URI
              valueFrom:
                configMapKeyRef:
                  name: fraud-api-config
                  key: MLFLOW_TRACKING_URI
          resources:
            requests:
              cpu: 250m
              memory: 512Mi
            limits:
              cpu: 500m
              memory: 1Gi
          startupProbe:
            httpGet:
              path: /health
              port: 8000
            failureThreshold: 30
            periodSeconds: 2
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            periodSeconds: 5
            failureThreshold: 3
          livenessProbe:
            httpGet:
              path: /health
              port: 8000
            periodSeconds: 10
            failureThreshold: 3
EOF
kubectl apply -f k8s/deployment-canary.yaml
kubectl get pods -l app=fraud-api
Vous devriez voir : 4 pods Running — 3 fraud-api (v0.1) et 1 fraud-api-canary (v0.2).You should see: 4 Running pods — 3 fraud-api (v0.1) and 1 fraud-api-canary (v0.2).
9Observer la répartition du traficWatch the traffic split

Envoyez 40 requêtes depuis l'intérieur du cluster (on vise /docs, que les probes ne touchent pas, pour un comptage propre), puis comptez ce qui est arrivé au canary dans ses logs. Send 40 requests from inside the cluster (we target /docs, which the probes never hit, for a clean count), then count what reached the canary in its logs.

kubectl run traffic --rm -it --restart=Never --image=busybox -- /bin/sh -c 'for i in $(seq 1 40); do wget -q -O /dev/null http://fraud-api/docs; done; echo done'
kubectl logs deployment/fraud-api-canary | grep -c 'GET /docs'
Vous devriez voir : « done », puis un compte autour de 10 — 1 pod sur 4, soit ~25 % du trafic réel sur la nouvelle version.You should see: "done", then a count around 10 — 1 pod out of 4, i.e. ~25% of real traffic on the new version.
10Promouvoir (rolling update) — ou retirerPromote (rolling update) — or withdraw

Les métriques tiennent ? Promotion : le Deployment stable passe en v0.2 par rolling update — un pod remplacé à la fois, jamais sous l'effectif — puis le canary est retiré. Si ça s'était mal passé : kubectl delete -f k8s/deployment-canary.yaml seul, et retour instantané à 100 % v0.1. Metrics hold? Promotion: the stable Deployment moves to v0.2 through a rolling update — one pod replaced at a time, never below strength — then the canary is withdrawn. Had it gone wrong: kubectl delete -f k8s/deployment-canary.yaml alone, and you are instantly back to 100% v0.1.

kubectl set image deployment/fraud-api fraud-api=fraud-api:0.2
kubectl rollout status deployment/fraud-api
kubectl delete -f k8s/deployment-canary.yaml
kubectl get deployment fraud-api -o wide
Vous devriez voir : « deployment "fraud-api" successfully rolled out », le canary supprimé, et la colonne IMAGES affichant fraud-api:0.2.You should see: "deployment "fraud-api" successfully rolled out", the canary deleted, and the IMAGES column showing fraud-api:0.2.
11Installer metrics-server (prérequis du HPA)Install metrics-server (HPA prerequisite)

Le HPA a besoin de mesures CPU. Sur kind, metrics-server refuse les certificats des kubelets — le patch --kubelet-insecure-tls est le réglage standard pour un cluster local. The HPA needs CPU measurements. On kind, metrics-server rejects the kubelets' certificates — the --kubelet-insecure-tls patch is the standard tweak for a local cluster.

kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
kubectl patch -n kube-system deployment metrics-server --type=json \
  -p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'
kubectl -n kube-system rollout status deployment/metrics-server
kubectl top nodes
Vous devriez voir : le tableau CPU / MEMORY du nœud. Si « Metrics API not available », attendez 30 s et relancez kubectl top nodes.You should see: the node's CPU / MEMORY table. If "Metrics API not available", wait 30 s and rerun kubectl top nodes.
12Le HPA, observé en montée et en descenteThe HPA, watched scaling up and down

Déclarez l'élastique (3 à 6 réplicas, cible 60 % de CPU), générez de la charge, et regardez. Si TARGETS ne décolle pas, lancez un second pod de charge (load2). La redescente, elle, prend ~5 minutes : l'HPA est prudent par conception. Declare the elastic (3 to 6 replicas, 60% CPU target), generate load, and watch. If TARGETS will not take off, start a second load pod (load2). Scaling down takes ~5 minutes: the HPA is cautious by design.

kubectl autoscale deployment fraud-api --cpu-percent=60 --min=3 --max=6
kubectl run load --image=busybox -- /bin/sh -c "while true; do wget -qO- http://fraud-api/health; done"
kubectl get hpa fraud-api -w
# Ctrl-C once REPLICAS goes above 3, then stop the load:
kubectl delete pod load
Vous devriez voir : TARGETS grimper au-dessus de 60 % et REPLICAS passer de 3 à 4-6 ; après suppression de la charge, retour à 3 au bout de quelques minutes.You should see: TARGETS climb above 60% and REPLICAS go from 3 to 4-6; once the load is deleted, back to 3 after a few minutes.

Mission 4 — Monitoring + drift (2h30)Mission 4 — Monitoring + drift (2h30)

13Exposer /metrics dans l'API — trois lignesExpose /metrics in the API — three lines

prometheus-fastapi-instrumentator instrumente toutes les routes et publie /metrics au format Prometheus. Une ligne dans requirements.txt, deux dans le code — c'est tout l'extrait. prometheus-fastapi-instrumentator instruments every route and publishes /metrics in Prometheus format. One line in requirements.txt, two in the code — that is the whole excerpt.

# requirements.txt: add this line
prometheus-fastapi-instrumentator

# src/serve/app.py: add these lines right after "app = FastAPI(...)"
from prometheus_fastapi_instrumentator import Instrumentator

Instrumentator().instrument(app).expose(app)
Vous devriez voir : après pip install -r requirements.txt, python -c "from src.serve.app import app" ne renvoie rien — aucune erreur d'import.You should see: after pip install -r requirements.txt, python -c "from src.serve.app import app" returns nothing — no import error.
14Reconstruire (v0.3), déployer, vérifier /metricsRebuild (v0.3), deploy, check /metrics

Le circuit devient un réflexe : build, kind load, set image, rollout. Si le port-forward de l'étape 6 est tombé pendant le rolling update (c'est normal), relancez kubectl port-forward service/fraud-api 8000:80 &. The circuit becomes a reflex: build, kind load, set image, rollout. If step 6's port-forward dropped during the rolling update (that is normal), rerun kubectl port-forward service/fraud-api 8000:80 &.

docker build -t fraud-api:0.3 .
kind load docker-image fraud-api:0.3 --name mlops
kubectl set image deployment/fraud-api fraud-api=fraud-api:0.3
kubectl rollout status deployment/fraud-api
curl -s http://localhost:8000/metrics | head -20
Vous devriez voir : des métriques http_requests_total et http_request_duration_seconds au format Prometheus.You should see: http_requests_total and http_request_duration_seconds metrics in Prometheus format.
15Prometheus, par manifest minimalPrometheus, from a minimal manifest

Trois objets dans un seul fichier : la ConfigMap de scrape (toutes les 15 s sur le Service fraud-api), le Deployment, le Service. Pas de Helm, pas de magie — juste du YAML lisible. Three objects in one file: the scrape ConfigMap (every 15 s against the fraud-api Service), the Deployment, the Service. No Helm, no magic — just readable YAML.

cat > k8s/prometheus.yaml <<'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-config
data:
  prometheus.yml: |
    global:
      scrape_interval: 15s
    scrape_configs:
      - job_name: fraud-api
        metrics_path: /metrics
        static_configs:
          - targets: ["fraud-api:80"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: prometheus
spec:
  replicas: 1
  selector:
    matchLabels:
      app: prometheus
  template:
    metadata:
      labels:
        app: prometheus
    spec:
      containers:
        - name: prometheus
          image: prom/prometheus:v2.53.0
          args:
            - --config.file=/etc/prometheus/prometheus.yml
          ports:
            - containerPort: 9090
          volumeMounts:
            - name: config
              mountPath: /etc/prometheus
      volumes:
        - name: config
          configMap:
            name: prometheus-config
---
apiVersion: v1
kind: Service
metadata:
  name: prometheus
spec:
  type: ClusterIP
  selector:
    app: prometheus
  ports:
    - port: 9090
      targetPort: 9090
EOF
kubectl apply -f k8s/prometheus.yaml
kubectl rollout status deployment/prometheus
kubectl port-forward service/prometheus 9090:9090 &
Vous devriez voir : sur http://localhost:9090/targets, le job fraud-api en State UP au bout de 15 secondes.You should see: on http://localhost:9090/targets, the fraud-api job in State UP within 15 seconds.
16Le capteur de drift : scripts/check_drift.pyThe drift sensor: scripts/check_drift.py

Le script compare la distribution des scores récents à une référence (test de Kolmogorov-Smirnov) et sort en code 1 si elle a bougé — exactement ce qu'un cron ou une CI sait transformer en alerte. On le teste avec une dérive fabriquée ; régénérez recent_scores avec beta(2, 20) pour voir le cas « OK ». The script compares the recent score distribution to a reference (Kolmogorov-Smirnov test) and exits with code 1 if it moved — exactly what a cron job or a CI can turn into an alert. We test it with a manufactured drift; regenerate recent_scores with beta(2, 20) to see the "OK" case.

pip install scipy numpy
mkdir -p scripts data
cat > scripts/check_drift.py <<'EOF'
"""Compare recent prediction scores against a reference distribution (KS test)."""
import json
import sys
from pathlib import Path

import numpy as np
from scipy import stats

REFERENCE = Path("data/reference_scores.json")
RECENT = Path("data/recent_scores.json")
P_THRESHOLD = 0.05


def load_scores(path):
    if not path.exists():
        print(f"missing file: {path}")
        sys.exit(2)
    return np.array(json.loads(path.read_text()))


def main():
    ref = load_scores(REFERENCE)
    recent = load_scores(RECENT)
    ks_stat, p_value = stats.ks_2samp(ref, recent)
    print(f"reference: n={len(ref)}  mean={ref.mean():.3f}  q95={np.quantile(ref, 0.95):.3f}")
    print(f"recent:    n={len(recent)}  mean={recent.mean():.3f}  q95={np.quantile(recent, 0.95):.3f}")
    print(f"KS statistic={ks_stat:.3f}  p-value={p_value:.4f}")
    if p_value < P_THRESHOLD:
        print("ALERT: score distribution has drifted")
        return 1
    print("OK: no significant drift detected")
    return 0


if __name__ == "__main__":
    sys.exit(main())
EOF
python - <<'EOF'
import json
import numpy as np
rng = np.random.default_rng(0)
json.dump(rng.beta(2, 20, 1000).tolist(), open("data/reference_scores.json", "w"))
json.dump(rng.beta(4, 10, 500).tolist(), open("data/recent_scores.json", "w"))
EOF
python scripts/check_drift.py
echo "exit code: $?"
Vous devriez voir : les deux distributions résumées, le test KS, « ALERT: score distribution has drifted » et exit code: 1.You should see: both distributions summarized, the KS test, "ALERT: score distribution has drifted" and exit code: 1.

Mission 5 — Rollback + observabilité (1h30)Mission 5 — Rollback + observability (1h30)

17Rollback niveau 1 : l'applicationRollback level 1: the application

Suspect n° 1 : le code. Kubernetes garde l'historique des rollouts — la marche arrière est une commande, à répéter à froid pour qu'elle soit un réflexe le jour J. Suspect number 1: the code. Kubernetes keeps the rollout history — reverse gear is one command, to be rehearsed in calm weather so it is a reflex on the day.

kubectl rollout history deployment/fraud-api
kubectl rollout undo deployment/fraud-api
kubectl rollout status deployment/fraud-api
kubectl get deployment fraud-api -o wide
Vous devriez voir : la colonne IMAGES revenue à fraud-api:0.2 — la version précédente, de retour en quelques secondes, sans coupure.You should see: the IMAGES column back to fraud-api:0.2 — the previous version, back in seconds, with zero downtime.
18Rollback niveau 2 : le modèle (alias MLflow)Rollback level 2: the model (MLflow alias)

Suspect n° 2 : le modèle. Le geste du bloc 5 : repointer l'alias @production du registry vers la version précédente — aucun rebuild, aucun manifest. Un rollout restart force les pods à recharger models:/fraud-detector@production. Suspect number 2: the model. Block 5's move: repoint the registry's @production alias to the previous version — no rebuild, no manifest. A rollout restart forces the pods to reload models:/fraud-detector@production.

python - <<'EOF'
from mlflow import MlflowClient

client = MlflowClient(tracking_uri="http://localhost:5000")
current = client.get_model_version_by_alias("fraud-detector", "production")
print("in production before:", current.version)
previous = str(int(current.version) - 1)
client.set_registered_model_alias("fraud-detector", "production", previous)
after = client.get_model_version_by_alias("fraud-detector", "production")
print("in production after:", after.version)
EOF
kubectl rollout restart deployment/fraud-api
kubectl rollout status deployment/fraud-api
Vous devriez voir : « in production after » = la version n-1, puis les pods redémarrer un à un — le cluster sert l'ancien modèle, l'incident est clos aux deux niveaux.You should see: "in production after" = version n-1, then the pods restart one by one — the cluster serves the previous model, the incident is closed on both levels.
Si ça coinceWhen it breaks

ImagePullBackOff — l'image n'est pas dans le cluster : rejouez kind load docker-image … --name mlops, et vérifiez imagePullPolicy: Never (sinon Kubernetes part la chercher sur Docker Hub). Pods tués en boucle pendant le chargement du modèle (RESTARTS qui monte, CrashLoopBackOff) — startupProbe absente ou trop courte : 30 × 2 s = 60 s de grâce, augmentez failureThreshold si votre modèle est plus lent. kubectl top ou HPA en « unknown » — metrics-server sans --kubelet-insecure-tls sur kind : rejouez le patch de l'étape 11. Le port-forward tombe (« lost connection to pod ») — normal quand le pod derrière meurt (rolling update, delete pod) : relancez simplement la commande. ImagePullBackOff — the image is not in the cluster: rerun kind load docker-image … --name mlops, and check imagePullPolicy: Never (otherwise Kubernetes goes looking on Docker Hub). Pods killed in a loop while the model loads (climbing RESTARTS, CrashLoopBackOff) — startupProbe missing or too short: 30 × 2 s = 60 s of grace, raise failureThreshold if your model is slower. kubectl top or HPA stuck on "unknown" — metrics-server without --kubelet-insecure-tls on kind: replay step 11's patch. The port-forward drops ("lost connection to pod") — normal whenever the pod behind it dies (rolling update, delete pod): just rerun the command.