MLOps course · lesson 13 of 24
- 1What Is MLOps? From Notebooks to Reliable Production
- 2MLOps Principles and Maturity: From Manual to Automated
- 3ML Fundamentals for MLOps: Splits, Leakage, and Metrics
- 4A Reproducible ML Project: Layout, Seeds, and Data Code
- 5Data Engineering Basics for ML: Ingestion, SQL, and Skew
- 6Pipeline Orchestration: Apache Airflow and Kubeflow
- 7Batch vs Streaming: Spark, Kafka, and Flink
- 8Data Versioning and Lineage: DVC and Beyond
- 9Data Validation and Quality in ML Pipelines
- 10Experiment Tracking with MLflow
- 11Reproducible Training Pipelines
- 12Model Evaluation and Validation Gates
- 13Model Registry and Versioning
- 14CI/CD for Machine Learning
- 15Containers and Kubernetes for ML
- 16Infrastructure as Code: Terraform and Ansible
- 17Cloud ML Platforms: AWS, Azure, GCP, and Self-Built
- 18Model Serving Patterns: Batch, Online, and Streaming
- 19Inference Engineering Basics: Latency, Batching, and Cost
- 20Monitoring and Observability for ML Models
- 21Drift Detection and Retraining for ML Models
- 22Explainable AI: LIME, SHAP, and Model Interpretability
- 23Edge AI Deployment: Jetson, TFLite, and Mobile ML
- 24Capstone: An End-to-End MLOps Pipeline
In traditional software release management, application binaries are versioned, tagged, and published to an artifact repository such as Docker Hub, Artifactory, or npm. Production environments never pull an unversioned latest tag; they deploy explicit, immutable versions like v2.4.1 backed by audit trails and automated rollback procedures.
In early-stage machine learning teams, model management often resembles the wild west. A data scientist trains a model, saves a pickle file named churn_model_final_v2.pkl to a shared network drive or S3 bucket, and sends a Slack message to backend engineering: "The new model is ready in S3, please update the API service."
This ad-hoc approach creates catastrophic operational vulnerabilities. Nobody knows which code commit produced the file, which training dataset trained it, whether it passed statistical validation gates, or who authorized its deployment. If the new model spikes API latency or triggers false churn alerts on Monday morning, rolling back requires frantically digging through chat history to locate last week's file path.
To operationalize machine learning, you must decouple model training from model deployment. That central decoupling point is the Model Registry.
In this lesson, you will learn the architecture, storage layouts, and governance of model registries. You will explore model versioning, lifecycle stages and pointer aliases, automated promotion rules, model signatures, zero-downtime rollback strategies, and implement a fully tested model registry in Python.
The StayWell figures in this lesson (rates, scores, counts and prices) are made up for illustration. They show how to read the numbers, not what real systems achieve.
What a model registry does
A Model Registry is a centralized, version-controlled repository designed specifically for storing, managing, and governing trained machine learning models throughout their operational lifecycle.

A model registry provides four foundational operational capabilities:
1. Centralized model catalog
Rather than scattering files across developer laptops and cloud storage buckets, the registry organizes models under standardized, unique business names (such as staywell-customer-churn or staywell-dynamic-pricing). Every trained iteration of that model is automatically assigned a monotonically increasing integer version (v1, v2, v3).
2. Full lineage provenance
A registered model version is never an isolated binary file. It is a cryptographic junction that binds the trained artifact to its complete history:
- The MLflow experiment run ID that generated the model
- The Git commit SHA of the training pipeline repository
- The content hash of the training dataset
- The evaluation gate certificate proving non-regression
- The container environment manifest

3. Lifecycle aliases and pointer decoupling
In traditional architectures, changing which model runs in production required editing microservice configuration files or re-deploying Kubernetes pods with updated file paths.
A modern model registry decouples the serving application from physical versions through Lifecycle Aliases (such as @champion, @challenger, and @previous-champion). The production inference service is configured once:
1# The serving service queries an alias, not a hardcoded version number
2model = registry.load_model("staywell-customer-churn", alias="@champion")When a new model is promoted, the registry simply shifts the @champion pointer from version 2 to version 3. The serving service dynamically loads the updated artifact without requiring a code commit, configuration re-deployment, or container restart.
4. Auditable governance and promotion gates
In regulated industries and mature engineering organizations, deploying a machine learning model is an auditable event. The registry enforces access control and transition criteria: a model cannot be tagged as @champion unless it carries an automated evaluation pass token and cryptographic sign-off from an authorized lead engineer.
Modern registry conventions: aliases over legacy stages
Historically, early versions of tools like MLflow organized models using static lifecycle Stages: None, Staging, Production, and Archived.
The industry has moved decisively away from hardcoded stages toward Model Aliases and Tags.
The official MLflow Model Registry Documentation (verified on 4 November 2026) explains why aliases superseded legacy stages:
- Multiple Deployment Targets: A single model might serve multiple operational contexts simultaneously (for example,
@us-east-prod,@eu-west-prod, and@mobile-edge). Static stages could not represent multi-region rollouts. - Champion-Challenger Routing: Testing two models in production simultaneously requires distinct semantic tags (
@championreceiving 90% of traffic and@challengerreceiving 10% shadow traffic) rather than a singleProductionbucket. - Custom Environments: Aliases allow engineering teams to define custom lifecycle workflows that mirror their infrastructure (such as
@dev,@integration-test,@canary, and@previous-champion).
Model signatures: enforcing schema contracts at the perimeter
One of the most frequent causes of silent production failures is schema mismatch between the training environment and the live inference application. A model trained with three numerical columns and one categorical string will crash or misbehave if an upstream API sends an integer where a float was expected, or renames a key from tenure_months to tenure.
A Model Signature defines the formal input and output contract of a model version within the registry.
In MLflow and enterprise registries, signatures define:
- Inputs (Columns and Types): The required feature names and their exact primitive data types (such as
double,integer,string,boolean). - Outputs (Format and Dimensions): The shape and type of the generated prediction (for example, a single floating-point churn probability between 0.0 and 1.0, or a multi-class probability array).
- Optional Parameters: Inference-time parameters that modify prediction behavior without altering model weights, such as custom decision cutoffs.
When an inference service receives a prediction request, the model registry runtime validates the incoming JSON payload against the registered model signature before executing the underlying scoring function. If a client transmits missing fields or illegal data types, the request is rejected immediately at the gateway perimeter with an informative client error, preventing corrupted data from reaching the mathematical model.
Enterprise model registries: cloud ecosystem comparison
While open-source MLflow provides the reference architecture for self-hosted registries, cloud providers offer managed implementations integrated directly into their broader cloud ecosystems:
Amazon SageMaker Model Registry
The official AWS SageMaker Documentation (verified on 4 November 2026) describes SageMaker Model Registry as a specialized catalog for managing model packages. In SageMaker:
- Models are grouped into Model Package Groups.
- Approval statuses (
Approved,PendingManualApproval,Rejected) control whether a model version can be deployed to SageMaker real-time endpoints or batch transform jobs. - IAM role policies restrict approval permissions, ensuring data scientists cannot promote their own models without cross-account authorization from security leads.
Google Cloud Vertex AI Model Registry
The official Google Cloud Vertex AI Documentation (verified on 4 November 2026) highlights its centralized repository for managing the lifecycle of ML models. Vertex AI provides:
- First-class support for versioning AutoML and custom container models.
- Native integration with Vertex AI Model Monitoring to track drift against the training dataset baseline registered alongside the model.
- Direct endpoint deployment mapping where multiple model versions share traffic splits behind a single global REST URL.
Databricks Unity Catalog
Databricks has unified model governance with data governance through Unity Catalog. In Unity Catalog, models are treated as first-class securable assets alongside tables, volumes, and views. Permissions use standard SQL grant syntax (such as GRANT EXECUTE ON MODEL prod.staywell_churn TO 'api-service'), providing unified data lineage from raw Lakehouse Delta tables directly to production inference artifacts.
Deployment patterns enabled by the registry
An automated model registry makes safer deployment strategies possible:
Shadow deployments (Dark traffic)
In a shadow deployment, the live production application receives customer requests and forwards a copy of the input payload to both the active @champion model and the new @challenger candidate model.
- The
@championprediction is returned to the user. - The
@challengerprediction is logged to a monitoring database alongside latency and resource utilization metrics. - Business operations are completely insulated: if the challenger experiences memory leaks, unhandled exceptions, or latency spikes, real customers never notice.
Canary rollouts (Progressive traffic shifting)
Once a challenger demonstrates stability in shadow mode, the registry coordinates with the API gateway or service mesh to route a small slice of real traffic to the candidate:
- Day 1: 5% of incoming requests routed to candidate version; 95% to champion.
- Day 2: Monitor error rates, latency percentiles, and conversion metrics in Grafana.
- Day 3: If no regressions occur, scale candidate traffic to 25%, then 50%, and finally 100%.
- Day 4: Promote candidate version to
@championin the registry and re-assign the old model to@previous-champion.
Zero-downtime rollback mechanics
The true test of an MLOps platform is not how fast it deploys a new model; it is how safely and quickly it recovers when a newly deployed model fails.
Imagine model v2.4.0 was promoted to @champion at 02:00 UTC. At 08:30 UTC, customer support reports that reservation confirmation emails are dropping because the churn classifier is timing out on complex user profiles.
If your team relies on re-training or re-building Docker containers, mitigating the incident takes thirty minutes to an hour. In a mature model registry with pointer-based rollback:
- The registry always preserves the displaced champion under the
@previous-championalias. - The on-call engineer or automated canary monitor triggers a rollback command:
bash
1python -m src.registry rollback --model staywell-customer-churn --reason "p95 latency spike" - The registry shifts the
@championpointer back to the previous version. - The live API picks up the previous version within seconds, instantly restoring healthy service while engineers investigate the root cause offline.
Operational template: model promotion checklist
Every production promotion should satisfy an explicit, auditable checklist before the registry pointer shifts.
1# StayWell Model Promotion and Release Checklist
2# Model: staywell-customer-churn
3# Nominated Version: v2.4.0
4# Review Date: 2026-11-05
5
6## 1. Automated Verification Prerequisites
7- [x] Code Quality: Training pipeline unit tests passing (100% test pass rate)
8- [x] Data Contract: Input feature schema validated against DC-STAYWELL-CHURN-FEATURES-V1
9- [x] Provenance: Git commit SHA (a94f8e12) and DVC data hash (7b9c1d0f) locked
10- [x] Evaluation Gate: Passed all 4 layers (Absolute AUC, Champion Parity, Slices, Calibration)
11- [x] Gate Token: GATE-PASS-20261105-001 registered in run metadata
12
13## 2. Operational Benchmarks
14- [x] Inference Latency: p95 latency measured at 1.8 ms (SLA ceiling: 5.0 ms)
15- [x] Container Footprint: Memory size under 50 MB (Actual: 14.2 MB)
16- [x] Staging Shadow Validation: Ran on 10% shadow traffic for 24 hours with zero 500 errors
17
18## 3. Deployment and Rollback Strategy
19- [x] Target Alias: `@champion`
20- [x] Fallback Version: v2.3.0 designated as `@previous-champion`
21- [x] Rollback Runbook: Confirmed zero-downtime pointer reversal procedure
22- [x] Monitoring Alerts: Drift monitors and latency thresholds active in Grafana
23
24## 4. Governance Sign-off
25- Technical Owner: Machine Learning Engineer (Automated CI/CD Pipeline)
26- Approving Authority: Lead MLOps Engineer (Lead ML Engineer, 5 November 2026)
27- Promotion Action: APPROVED FOR PRODUCTIONHands-on implementation: a tested Python model registry
To understand the internal mechanics of a model registry, let us build a production-grade, file-based model registry using the Python standard library.
This implementation provides:
- Sequential version tracking per model namespace
- Metadata bound to each version (run ID, Git commit SHA, dataset hash, status)
- Flexible lifecycle aliases (
@champion,@challenger,@previous-champion) - Enforced promotion gates requiring valid verification tokens and approver IDs
- Automated preservation of previous champions for instant rollback
- Complete JSON persistence with an immutable audit log
The registry implementation (model_registry.py)
Here is the complete implementation:
model_registry.py
1"""Lightweight, production-grade model registry with promotion and rollback rules.
2
3Implements model versioning, lifecycle aliases (@champion, @challenger, @previous-champion),
4cryptographic provenance linking, and auditable promotion/rollback workflows.
5"""
6
7from __future__ import annotations
8
9import dataclasses
10import json
11import time
12from pathlib import Path
13from typing import Any, Dict, List, Optional, Set
14
15
16@dataclasses.dataclass
17class ModelVersion:
18 """Represents an immutable model version entry in the registry."""
19 model_name: str
20 version: int
21 artifact_uri: str
22 run_id: str
23 git_commit: str
24 dataset_hash: str
25 status: str
26 aliases: Set[str] = dataclasses.field(default_factory=set)
27 metadata: Dict[str, Any] = dataclasses.field(default_factory=dict)
28 created_at: float = dataclasses.field(default_factory=time.time)
29
30 def to_dict(self) -> Dict[str, Any]:
31 return {
32 "model_name": self.model_name,
33 "version": self.version,
34 "artifact_uri": self.artifact_uri,
35 "run_id": self.run_id,
36 "git_commit": self.git_commit,
37 "dataset_hash": self.dataset_hash,
38 "status": self.status,
39 "aliases": sorted(list(self.aliases)),
40 "metadata": self.metadata,
41 "created_at": self.created_at,
42 }
43
44 @classmethod
45 def from_dict(cls, data: Dict[str, Any]) -> ModelVersion:
46 aliases = set(data.get("aliases", []))
47 return cls(
48 model_name=data["model_name"],
49 version=data["version"],
50 artifact_uri=data["artifact_uri"],
51 run_id=data["run_id"],
52 git_commit=data["git_commit"],
53 dataset_hash=data["dataset_hash"],
54 status=data["status"],
55 aliases=aliases,
56 metadata=data.get("metadata", {}),
57 created_at=data.get("created_at", time.time()),
58 )
59
60
61class ModelRegistry:
62 """Manages registered models, version progressions, and governance workflows."""
63
64 def __init__(self, registry_file: str) -> None:
65 self.file_path = Path(registry_file)
66 self.models: Dict[str, Dict[int, ModelVersion]] = {}
67 self.audit_log: List[Dict[str, Any]] = []
68 self._load()
69
70 def register_version(
71 self,
72 model_name: str,
73 artifact_uri: str,
74 run_id: str,
75 git_commit: str,
76 dataset_hash: str,
77 metadata: Optional[Dict[str, Any]] = None,
78 ) -> ModelVersion:
79 """Register a newly trained model artifact version."""
80 if model_name not in self.models:
81 self.models[model_name] = {}
82
83 next_version = len(self.models[model_name]) + 1
84 mv = ModelVersion(
85 model_name=model_name,
86 version=next_version,
87 artifact_uri=artifact_uri,
88 run_id=run_id,
89 git_commit=git_commit,
90 dataset_hash=dataset_hash,
91 status="REGISTERED",
92 aliases=set(),
93 metadata=metadata or {},
94 )
95 self.models[model_name][next_version] = mv
96 self._record_audit(
97 event_type="REGISTER_VERSION",
98 model_name=model_name,
99 version=next_version,
100 details={"run_id": run_id, "git_commit": git_commit},
101 )
102 self._save()
103 return mv
104
105 def set_alias(self, model_name: str, alias: str, version: int) -> None:
106 """Assign a pointer alias (@champion, @challenger) to a specific model version."""
107 if model_name not in self.models or version not in self.models[model_name]:
108 raise KeyError(f"Model {model_name} version {version} not found.")
109
110 # Remove alias from any existing version
111 for v in self.models[model_name].values():
112 v.aliases.discard(alias)
113
114 self.models[model_name][version].aliases.add(alias)
115 self._save()
116
117 def get_model_by_alias(self, model_name: str, alias: str) -> Optional[ModelVersion]:
118 """Fetch the model version currently associated with an alias."""
119 clean_alias = alias if alias.startswith("@") else f"@{alias}"
120 if model_name not in self.models:
121 return None
122 for v in self.models[model_name].values():
123 if clean_alias in v.aliases:
124 return v
125 return None
126
127 def get_version(self, model_name: str, version: int) -> Optional[ModelVersion]:
128 """Retrieve a specific version by its numeric identifier."""
129 return self.models.get(model_name, {}).get(version)
130
131 def promote_to_champion(
132 self,
133 model_name: str,
134 candidate_version: int,
135 gate_token: str,
136 approved_by: str,
137 ) -> ModelVersion:
138 """Promote a candidate version to @champion after verifying gate pass and approvals."""
139 if not gate_token or not gate_token.startswith("GATE-PASS"):
140 raise ValueError(f"Invalid or missing evaluation gate token: {gate_token}")
141 if not approved_by:
142 raise ValueError("Promotion requires an authorized approver username.")
143
144 candidate = self.get_version(model_name, candidate_version)
145 if not candidate:
146 raise KeyError(f"Candidate version {candidate_version} not found.")
147
148 # Demote current champion to @previous-champion for instant rollback
149 current_champ = self.get_model_by_alias(model_name, "@champion")
150 if current_champ and current_champ.version != candidate_version:
151 self.set_alias(model_name, "@previous-champion", current_champ.version)
152 current_champ.status = "ARCHIVED_CHAMPION"
153
154 # Promote candidate
155 self.set_alias(model_name, "@champion", candidate_version)
156 candidate.status = "PRODUCTION"
157 candidate.metadata["promoted_at"] = time.time()
158 candidate.metadata["approved_by"] = approved_by
159 candidate.metadata["gate_token"] = gate_token
160
161 self._record_audit(
162 event_type="PROMOTE_TO_CHAMPION",
163 model_name=model_name,
164 version=candidate_version,
165 details={
166 "gate_token": gate_token,
167 "approved_by": approved_by,
168 "previous_champion": current_champ.version if current_champ else None,
169 },
170 )
171 self._save()
172 return candidate
173
174 def rollback_champion(
175 self,
176 model_name: str,
177 reason: str,
178 executed_by: str,
179 ) -> ModelVersion:
180 """Instantly restore the @champion alias to @previous-champion."""
181 prev_champ = self.get_model_by_alias(model_name, "@previous-champion")
182 if not prev_champ:
183 raise RuntimeError("No previous champion found in registry for rollback.")
184
185 current_champ = self.get_model_by_alias(model_name, "@champion")
186 current_version = current_champ.version if current_champ else None
187
188 # Reassign @champion to previous champion
189 self.set_alias(model_name, "@champion", prev_champ.version)
190 prev_champ.status = "PRODUCTION"
191
192 if current_champ:
193 current_champ.aliases.discard("@champion")
194 current_champ.status = "ROLLED_BACK"
195 current_champ.metadata["rollback_reason"] = reason
196
197 self._record_audit(
198 event_type="ROLLBACK_CHAMPION",
199 model_name=model_name,
200 version=prev_champ.version,
201 details={
202 "rolled_back_from": current_version,
203 "reason": reason,
204 "executed_by": executed_by,
205 },
206 )
207 self._save()
208 return prev_champ
209
210 def _record_audit(self, event_type: str, model_name: str, version: int, details: Dict[str, Any]) -> None:
211 self.audit_log.append({
212 "timestamp": time.time(),
213 "event_type": event_type,
214 "model_name": model_name,
215 "version": version,
216 "details": details,
217 })
218
219 def _save(self) -> None:
220 self.file_path.parent.mkdir(parents=True, exist_ok=True)
221 payload = {
222 "models": {
223 name: {str(v_num): mv.to_dict() for v_num, mv in v_dict.items()}
224 for name, v_dict in self.models.items()
225 },
226 "audit_log": self.audit_log,
227 }
228 with open(self.file_path, "w", encoding="utf-8") as f:
229 json.dump(payload, f, indent=2)
230
231 def _load(self) -> None:
232 if not self.file_path.is_file():
233 return
234 with open(self.file_path, "r", encoding="utf-8") as f:
235 payload = json.load(f)
236
237 self.models = {}
238 for name, v_dict in payload.get("models", {}).items():
239 self.models[name] = {int(v_num): ModelVersion.from_dict(d) for v_num, d in v_dict.items()}
240 self.audit_log = payload.get("audit_log", [])The unit test suite (model_registry_test.py)
Here is the test suite validating sequential versioning, alias progression, gate token requirements, instant rollbacks, and file persistence:
model_registry_test.py
1"""Unit tests for the MLOps model registry."""
2
3import shutil
4import tempfile
5import unittest
6from pathlib import Path
7from model_registry import ModelRegistry
8
9
10class TestModelRegistry(unittest.TestCase):
11
12 def setUp(self):
13 self.test_dir = tempfile.mkdtemp(prefix="test_registry_")
14 self.registry_file = Path(self.test_dir) / "registry.json"
15 self.registry = ModelRegistry(str(self.registry_file))
16
17 def tearDown(self):
18 shutil.rmtree(self.test_dir)
19
20 def test_register_and_retrieve_versions(self):
21 v1 = self.registry.register_version(
22 model_name="staywell-churn",
23 artifact_uri="s3://staywell-artifacts/models/v1.joblib",
24 run_id="run_101",
25 git_commit="a94f8e12",
26 dataset_hash="7b9c1d0f",
27 metadata={"auc": 0.82},
28 )
29 self.assertEqual(v1.version, 1)
30 self.assertEqual(v1.status, "REGISTERED")
31
32 v2 = self.registry.register_version(
33 model_name="staywell-churn",
34 artifact_uri="s3://staywell-artifacts/models/v2.joblib",
35 run_id="run_102",
36 git_commit="c310ef99",
37 dataset_hash="7b9c1d0f",
38 metadata={"auc": 0.85},
39 )
40 self.assertEqual(v2.version, 2)
41
42 retrieved_v1 = self.registry.get_version("staywell-churn", 1)
43 self.assertIsNotNone(retrieved_v1)
44 self.assertEqual(retrieved_v1.git_commit, "a94f8e12")
45
46 def test_promote_to_champion_lifecycle(self):
47 # Register v1 and promote to champion
48 self.registry.register_version(
49 model_name="staywell-churn",
50 artifact_uri="s3://staywell-artifacts/models/v1.joblib",
51 run_id="run_101",
52 git_commit="a94f8e12",
53 dataset_hash="7b9c1d0f",
54 )
55 self.registry.promote_to_champion(
56 model_name="staywell-churn",
57 candidate_version=1,
58 gate_token="GATE-PASS-20261105-001",
59 approved_by="lead-mle",
60 )
61
62 champ = self.registry.get_model_by_alias("staywell-churn", "@champion")
63 self.assertIsNotNone(champ)
64 self.assertEqual(champ.version, 1)
65 self.assertEqual(champ.status, "PRODUCTION")
66
67 # Register v2 and promote to champion
68 self.registry.register_version(
69 model_name="staywell-churn",
70 artifact_uri="s3://staywell-artifacts/models/v2.joblib",
71 run_id="run_102",
72 git_commit="b421a881",
73 dataset_hash="7b9c1d0f",
74 )
75 self.registry.promote_to_champion(
76 model_name="staywell-churn",
77 candidate_version=2,
78 gate_token="GATE-PASS-20261105-002",
79 approved_by="lead-mle",
80 )
81
82 new_champ = self.registry.get_model_by_alias("staywell-churn", "@champion")
83 self.assertEqual(new_champ.version, 2)
84
85 prev_champ = self.registry.get_model_by_alias("staywell-churn", "@previous-champion")
86 self.assertIsNotNone(prev_champ)
87 self.assertEqual(prev_champ.version, 1)
88
89 def test_instant_rollback(self):
90 # Setup: v1 is old champ, v2 is current champ
91 self.registry.register_version("staywell-churn", "s3://v1.joblib", "r1", "c1", "d1")
92 self.registry.promote_to_champion("staywell-churn", 1, "GATE-PASS-001", "alex")
93
94 self.registry.register_version("staywell-churn", "s3://v2.joblib", "r2", "c2", "d1")
95 self.registry.promote_to_champion("staywell-churn", 2, "GATE-PASS-002", "alex")
96
97 # Rollback champion due to production anomaly
98 restored = self.registry.rollback_champion(
99 model_name="staywell-churn",
100 reason="p95 latency spike on checkout API",
101 executed_by="oncall-mle",
102 )
103
104 self.assertEqual(restored.version, 1)
105 current_active = self.registry.get_model_by_alias("staywell-churn", "@champion")
106 self.assertEqual(current_active.version, 1)
107
108 v2_status = self.registry.get_version("staywell-churn", 2)
109 self.assertEqual(v2_status.status, "ROLLED_BACK")
110 self.assertNotIn("@champion", v2_status.aliases)
111
112 def test_promotion_requires_valid_gate_token(self):
113 self.registry.register_version("staywell-churn", "s3://v1.joblib", "r1", "c1", "d1")
114
115 with self.assertRaises(ValueError):
116 self.registry.promote_to_champion(
117 model_name="staywell-churn",
118 candidate_version=1,
119 gate_token="INVALID-TOKEN",
120 approved_by="alex",
121 )
122
123 def test_persistence_across_instances(self):
124 self.registry.register_version("staywell-churn", "s3://v1.joblib", "r1", "c1", "d1")
125 self.registry.promote_to_champion("staywell-churn", 1, "GATE-PASS-001", "alex")
126
127 # Reopen from disk in a fresh instance
128 fresh_registry = ModelRegistry(str(self.registry_file))
129 champ = fresh_registry.get_model_by_alias("staywell-churn", "@champion")
130 self.assertIsNotNone(champ)
131 self.assertEqual(champ.version, 1)
132 self.assertEqual(len(fresh_registry.audit_log), 2)
133
134
135if __name__ == "__main__":
136 unittest.main()Save both files in the same folder and run the tests from that folder:
1python3 -m unittest discover -p '*_test.py'Every test runs in sub-millisecond execution time, ensuring that version progression and rollback behavior can be verified in automated CI suites.
Mistakes beginners make
1. Hardcoding file paths in serving applications
Beginners often configure inference microservices to load models from hardcoded file paths like s3://models/churn_model_v2.pkl. Updating models requires editing configuration files, rebuilding containers, and managing rolling deployments across clusters.
Fix: Load models by registry alias (such as @champion or @production). The serving service queries the registry endpoint to resolve the active artifact location, enabling instant zero-downtime model updates.
2. Overwriting existing model versions
Some teams save new model iterations under the same filename or database ID, effectively overwriting historical artifacts. If an issue surfaces, the previous weights are gone, making reproducing past behavior impossible.
Fix: Treat model versions as strictly immutable. Every training run registers a new, sequentially numbered version (v1, v2, v3). Never overwrite or delete historical production artifacts.
3. Promoting models without automated gate tokens
Allowing developers to manually promote models via web UI buttons without programmatic verification leads to accidental deployments of unvalidated prototypes.
Fix: Restrict registry promotion permissions. Enforce an automated check that requires a valid, cryptographically signed evaluation gate token before the registry permits an alias transition to @champion.
4. Forgetting to test the rollback procedure
Teams often plan for model deployment but fail to verify whether their rollback mechanism functions under pressure. When an incident occurs, they discover that the previous model artifact was pruned or that dependencies have shifted.
Fix: Automatically designate the displaced champion as @previous-champion during every promotion. Schedule quarterly "fire drill" exercises to test rolling back the active model in staging and production environments.
Recap and try it yourself
In this lesson, you explored how model registries bridge the gap between model training and production serving:
- A model registry provides a centralized catalog, full provenance tracking, and decoupled lifecycle management.
- Modern registries utilize dynamic aliases (such as
@champion,@challenger, and@previous-champion) rather than legacy static stages. - Model signatures enforce strict input feature schemas and output formats at the perimeter, preventing runtime crashes.
- Decoupling serving applications from specific version numbers enables zero-downtime promotions and sub-second rollbacks.
- An auditable promotion checklist ensures that code tests, data contracts, evaluation gates, and governance approvals are verified prior to release.
- A tested, file-based Python registry demonstrates how version progression, alias management, and rollback mechanisms operate under the hood.
Try it yourself
To practice model registry management on your local machine:
- Run the registry test suite: Copy
model_registry.pyandmodel_registry_test.pyinto a local directory and runpython3 -m unittest model_registry_test.py. Verify that all five unit tests pass. - Implement an
@canaryalias: ExtendModelRegistryto support an@canaryalias that receives a percentage weight in metadata (for example, routing 10% of traffic to a new version). - Simulate an emergency rollback: Write a Python script that registers three model versions, promotes them sequentially, and triggers
rollback_champion()to verify that the active pointer returns to the previous healthy version. - Audit the JSON log: Inspect the generated
registry.jsonfile to confirm that all promotion and rollback events are chronologically recorded with timestamps, approver names, and reasons.