Lab 03 — CI/CD Deployment Pipeline: Eval Gates, Canary & Rollback
Phase: 17 — MLOps Platform Difficulty: ⭐⭐⭐☆☆ (the control flow is small; the invariant — never ship a worse model — is ⭐⭐⭐⭐⭐) Time: 3–4 hours
The most expensive outage in ML is the one you cause yourself: a new model passes a notebook eval, gets pushed to 100% of traffic, and quietly tanks revenue for six hours before anyone notices. Safe deployment is the discipline that makes that impossible — gate on offline quality, canary a tiny slice of real traffic, watch its health, and roll back instantly if it misbehaves. This lab builds that machine from primitives: an
EvalGate(min/max thresholds + no-regression-vs-champion), aCanaryControllerthat ramps5% → 25% → 50% → 100%and freezes traffic the moment a stage is unhealthy,rollback/blue_green_swaprecovery, and aDeploymentPipelineorchestrator. Two invariants are the soul of the lab: the canary aborts and stays at the breaching stage when a health check fails midway, and the pipeline halts at the first failed gate and NEVER promotes a failing candidate.
What you build
EvalGate— the offline bar.evaluate(metrics) -> GateResultchecks min thresholds (accuracy>=,safety>=), max thresholds (latency_ms<=,cost<=), and a no-regression check vs the currently-shipping baseline (with absolute tolerance andlower_is_betterdirection).GateResultcarriespassedplus a per-criterionreasonsmap so a failure tells you exactly which bar it missed.CanaryController— progressive traffic shifting overstages = [5, 25, 50, 100].step(health_metrics)advances if healthy, else aborts and freezes at the last good traffic..status ∈ {in_progress, promoted, aborted},.current_traffic.rollback/blue_green_swap— restore a previous version (no mutation); swap blue↔green only if the candidate is healthy, else keep active.DeploymentPipeline— ordered gated stages (eval_gate → canary → promote).run(candidate, context) -> PipelineResultrecords per-stage history, halts at the first failed gate, and rolls back if a failure occurs after the candidate already became active.
Key concepts
| Concept | What to understand |
|---|---|
| Eval gate vs canary | the gate is offline (held-out metrics); the canary is online (real traffic on a small slice). You need both — offline metrics lie about production |
| No-regression check | "better on average" isn't enough; a candidate that beats thresholds but is worse than the champion is a regression. This is where lab-02 detect_regression plugs in |
| Progressive delivery | ramp traffic so a bad model hurts 5%, not 100%, of users before you catch it |
| Freeze-on-abort | the off-by-one that matters: a failing canary stays at the last good traffic — you never advance into the stage that just failed |
| Halt-at-first-failure | a pipeline that "logs the failure and keeps going" promotes broken models. The gate must stop the line |
| Roll back if past promote | if a later stage fails after cutover, restore the previous active version — the pipeline owns recovery, not a human |
| Determinism | no wall-clock, no randomness — the same candidate + context always yields the same decision, which is what makes the gate auditable |
Files
| File | Purpose |
|---|---|
lab.py | skeleton with # TODO markers — your implementation |
solution.py | complete reference; python solution.py runs a worked example |
test_lab.py | the proof — run it red, make it green |
requirements.txt | pytest only (pure stdlib otherwise) |
Run
pip install -r requirements.txt
pytest test_lab.py -v # against your lab.py
LAB_MODULE=solution pytest test_lab.py -v # against the reference
python solution.py # the worked example
Success criteria
- All tests pass against your implementation (and
LAB_MODULE=solution). - You can explain why
test_canary_aborts_and_freezes_at_breaching_stageandtest_pipeline_halts_at_first_failed_gate_and_does_not_promoteare the soul of the lab — and what production disaster each prevents. - You can explain why an offline
EvalGateis necessary but not sufficient, and what the canary catches that the gate can't. - You can articulate the no-regression check: why "passes thresholds" is weaker than "beats the champion", and the tolerance boundary semantics.
- You can trace, for a candidate that fails the eval gate, exactly which stages run (only
eval_gate), whatactiveends up as (unchanged), and why. - You can explain when the pipeline rolls back vs simply halts (only when it had already promoted).
How this maps to the real stack
| The miniature | The production mechanism | Where to verify it |
|---|---|---|
EvalGate | the offline-eval job in CI that compares candidate vs champion before anything ships | a GitHub Actions job running your eval suite and failing the build on a regression; MLflow run comparison |
EvalGate no-regression + tolerance | the model-quality gate keyed off the registry's current champion | MLflow Model Registry stage transition (Staging → Production) guarded by a metric check |
CanaryController (traffic %, abort) | progressive traffic shifting with automated analysis and rollback | Argo Rollouts Canary strategy with AnalysisTemplate; Flagger; Seldon Core / KServe canary trafficPercent |
step health check | the metric query (error rate, p95, business KPI) that gates each step | Argo Rollouts analysis querying Prometheus; KServe canary metrics |
blue_green_swap | atomic cutover between two full environments | Argo Rollouts BlueGreen strategy; a load-balancer/ingress target swap |
rollback | one-command revert to the prior model version | kubectl argo rollouts undo; MLflow re-promoting the previous version; a Git revert + redeploy |
DeploymentPipeline | the end-to-end promotion workflow stitching gates together | a GitHub Actions / Argo Workflows pipeline: eval → canary → promote |
Limits of the miniature (own these in the interview): real canaries run for minutes
to hours with statistical analysis (is the canary's error rate significantly worse?
sequential tests, not a single boolean), traffic splitting is done by a service mesh /
ingress you don't control from Python, and health signals are often delayed (the
business metric you care about lags the deploy). Promotion is rarely a clean function
call — it's a kubectl/registry API mutation with its own failure modes, and a real
rollback must drain in-flight requests and warm caches. This lab models the control
logic and invariants (gate → canary → promote, abort-and-freeze, halt-and-don't-promote,
roll-back-if-past-promote); the orchestration substrate (Argo, Flagger, KServe) and the
statistics (the sequential health test) are the extensions.
Extensions (build these toward a real rollout)
- Replace the boolean
health_checkwith a statistical canary analysis: compare canary vs baseline error rates with a two-proportion test (or reuse lab-02'sMetricMonitor/detect_regression) and abort on a significant degradation. - Add a time/observation budget per stage (e.g. require N healthy observations before advancing) instead of a single boolean.
- Add automatic rollback with drain: model in-flight requests and only complete the rollback once the old version is fully reinstated.
- Emit the pipeline run as a structured audit record (stage, decision, metrics, timestamp source = injected, not wall-clock) suitable for a deployment log.
- Wire the
EvalGateno-regression check to read two MLflow runs and the canary to emit an Argo Rollouts-style step plan.
Interview / resume
- Talking points: "Walk me through how you ship a new model to production safely." "Your candidate beats the offline metrics but the canary's error rate is up 0.5% — what do you do?" "What's the difference between blue/green and canary, and when do you pick each?" "How does your pipeline guarantee you never promote a model that fails a gate?"
- Resume bullet: Built a model-deployment pipeline with offline eval gates (min/max/no-regression vs champion), a progressive canary controller with abort-and-freeze semantics, and blue/green + rollback recovery — encoding the invariants (halt at first failed gate, never promote a failing candidate) behind Argo Rollouts / KServe-style progressive delivery.