{"slug":"mlops-production-pipeline","title":"Production MLOps: From Jupyter Notebook to Scalable Model Serving","description":"Building an end-to-end MLOps pipeline with Kubeflow, MLflow, Seldon Core, and automated drift detection for production ML systems","date":"2025-04-05","author":"Elaph Hilful","thumbnail":"/blog-images/mlops-pipeline.svg","tags":["MLOps","Kubernetes","Machine Learning","Kubeflow","AIOps"],"category":"MLOps","featured":false,"readTime":6,"htmlContent":"<p>The gap between training a model in a Jupyter notebook and serving it reliably in production is enormous. Most ML models never make it to production — not because they don't perform well, but because organizations lack the infrastructure to deploy, monitor, and maintain them at scale.</p>\n<p>This guide covers building a production MLOps pipeline that serves 15+ models with sub-hour deployment times and automated drift detection.</p>\n<p><img src=\"/blog-images/mlops-pipeline.svg\" alt=\"Production MLOps Pipeline Architecture\" loading=\"lazy\" decoding=\"async\"></p>\n<h2>The MLOps Maturity Gap</h2>\n<p>Here's what most ML teams deal with:</p>\n<ul>\n<li><strong>Level 0</strong>: Data scientists train models in notebooks, hand off pickled files to engineers</li>\n<li><strong>Level 1</strong>: Basic CI/CD for model code, manual deployment</li>\n<li><strong>Level 2</strong>: Automated training pipelines, model registry, basic monitoring</li>\n<li><strong>Level 3</strong>: Full automation — continuous training, serving, monitoring, and retraining</li>\n</ul>\n<p>Most organizations are stuck between Level 0 and Level 1. Our pipeline operates at Level 3.</p>\n<h2>Pipeline Architecture</h2>\n<h3>Stage 1: Data Versioning with DVC</h3>\n<p>Every ML experiment must be reproducible. DVC (Data Version Control) tracks datasets alongside code:</p>\n<pre role=\"region\" aria-label=\"yaml code block\"><code class=\"hljs language-yaml\" tabindex=\"0\"><span class=\"hljs-comment\"># dvc.yaml - Pipeline definition</span>\n<span class=\"hljs-attr\">stages:</span>\n  <span class=\"hljs-attr\">prepare:</span>\n    <span class=\"hljs-attr\">cmd:</span> <span class=\"hljs-string\">python</span> <span class=\"hljs-string\">src/prepare.py</span>\n    <span class=\"hljs-attr\">deps:</span>\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">src/prepare.py</span>\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">data/raw/</span>\n    <span class=\"hljs-attr\">params:</span>\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">prepare.split_ratio</span>\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">prepare.seed</span>\n    <span class=\"hljs-attr\">outs:</span>\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">data/processed/train.parquet</span>\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">data/processed/test.parquet</span>\n\n  <span class=\"hljs-attr\">train:</span>\n    <span class=\"hljs-attr\">cmd:</span> <span class=\"hljs-string\">python</span> <span class=\"hljs-string\">src/train.py</span>\n    <span class=\"hljs-attr\">deps:</span>\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">src/train.py</span>\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">data/processed/train.parquet</span>\n    <span class=\"hljs-attr\">params:</span>\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">train.learning_rate</span>\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">train.epochs</span>\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">train.batch_size</span>\n    <span class=\"hljs-attr\">outs:</span>\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">models/model.onnx</span>\n    <span class=\"hljs-attr\">metrics:</span>\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-attr\">metrics/train_metrics.json:</span>\n          <span class=\"hljs-attr\">cache:</span> <span class=\"hljs-literal\">false</span>\n\n  <span class=\"hljs-attr\">evaluate:</span>\n    <span class=\"hljs-attr\">cmd:</span> <span class=\"hljs-string\">python</span> <span class=\"hljs-string\">src/evaluate.py</span>\n    <span class=\"hljs-attr\">deps:</span>\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">src/evaluate.py</span>\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">models/model.onnx</span>\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">data/processed/test.parquet</span>\n    <span class=\"hljs-attr\">metrics:</span>\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-attr\">metrics/eval_metrics.json:</span>\n          <span class=\"hljs-attr\">cache:</span> <span class=\"hljs-literal\">false</span>\n    <span class=\"hljs-attr\">plots:</span>\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">metrics/confusion_matrix.csv</span>\n</code></pre>\n<h3>Stage 2: Training Orchestration with Kubeflow</h3>\n<p>Kubeflow Pipelines provides scalable, reproducible training on Kubernetes:</p>\n<pre role=\"region\" aria-label=\"python code block\"><code class=\"hljs language-python\" tabindex=\"0\"><span class=\"hljs-comment\"># pipelines/training_pipeline.py</span>\n<span class=\"hljs-keyword\">from</span> kfp <span class=\"hljs-keyword\">import</span> dsl, compiler\n<span class=\"hljs-keyword\">from</span> kfp.dsl <span class=\"hljs-keyword\">import</span> Input, Output, Dataset, Model, Metrics\n\n<span class=\"hljs-meta\">@dsl.component(<span class=\"hljs-params\">\n    base_image=<span class=\"hljs-string\">\"python:3.11-slim\"</span>,\n    packages_to_install=[<span class=\"hljs-string\">\"pandas\"</span>, <span class=\"hljs-string\">\"scikit-learn\"</span>, <span class=\"hljs-string\">\"mlflow\"</span>]\n</span>)</span>\n<span class=\"hljs-keyword\">def</span> <span class=\"hljs-title function_\">train_model</span>(<span class=\"hljs-params\">\n    train_data: Input[Dataset],\n    hyperparams: <span class=\"hljs-built_in\">dict</span>,\n    model_artifact: Output[Model],\n    metrics: Output[Metrics],\n</span>):\n    <span class=\"hljs-keyword\">import</span> pandas <span class=\"hljs-keyword\">as</span> pd\n    <span class=\"hljs-keyword\">from</span> sklearn.ensemble <span class=\"hljs-keyword\">import</span> GradientBoostingClassifier\n    <span class=\"hljs-keyword\">from</span> sklearn.metrics <span class=\"hljs-keyword\">import</span> accuracy_score, f1_score\n    <span class=\"hljs-keyword\">import</span> mlflow\n    <span class=\"hljs-keyword\">import</span> joblib\n\n    <span class=\"hljs-comment\"># Load data</span>\n    df = pd.read_parquet(train_data.path)\n    X = df.drop(<span class=\"hljs-string\">'target'</span>, axis=<span class=\"hljs-number\">1</span>)\n    y = df[<span class=\"hljs-string\">'target'</span>]\n\n    <span class=\"hljs-comment\"># Train with hyperparameters</span>\n    model = GradientBoostingClassifier(\n        learning_rate=hyperparams[<span class=\"hljs-string\">'learning_rate'</span>],\n        n_estimators=hyperparams[<span class=\"hljs-string\">'n_estimators'</span>],\n        max_depth=hyperparams[<span class=\"hljs-string\">'max_depth'</span>],\n    )\n    model.fit(X, y)\n\n    <span class=\"hljs-comment\"># Log metrics</span>\n    predictions = model.predict(X)\n    accuracy = accuracy_score(y, predictions)\n    f1 = f1_score(y, predictions, average=<span class=\"hljs-string\">'weighted'</span>)\n\n    metrics.log_metric(<span class=\"hljs-string\">'accuracy'</span>, accuracy)\n    metrics.log_metric(<span class=\"hljs-string\">'f1_score'</span>, f1)\n\n    <span class=\"hljs-comment\"># Save model</span>\n    joblib.dump(model, model_artifact.path)\n\n    <span class=\"hljs-comment\"># Log to MLflow</span>\n    <span class=\"hljs-keyword\">with</span> mlflow.start_run():\n        mlflow.log_params(hyperparams)\n        mlflow.log_metrics({<span class=\"hljs-string\">'accuracy'</span>: accuracy, <span class=\"hljs-string\">'f1'</span>: f1})\n        mlflow.sklearn.log_model(model, <span class=\"hljs-string\">\"model\"</span>)\n\n\n<span class=\"hljs-meta\">@dsl.component(<span class=\"hljs-params\">\n    base_image=<span class=\"hljs-string\">\"python:3.11-slim\"</span>,\n    packages_to_install=[<span class=\"hljs-string\">\"evidently\"</span>, <span class=\"hljs-string\">\"pandas\"</span>]\n</span>)</span>\n<span class=\"hljs-keyword\">def</span> <span class=\"hljs-title function_\">validate_model</span>(<span class=\"hljs-params\">\n    model: Input[Model],\n    test_data: Input[Dataset],\n    baseline_data: Input[Dataset],\n    validation_result: Output[Metrics],\n</span>) -&gt; <span class=\"hljs-built_in\">bool</span>:\n    <span class=\"hljs-string\">\"\"\"Validate model against quality gates before promotion.\"\"\"</span>\n    <span class=\"hljs-keyword\">import</span> pandas <span class=\"hljs-keyword\">as</span> pd\n    <span class=\"hljs-keyword\">from</span> evidently.test_suite <span class=\"hljs-keyword\">import</span> TestSuite\n    <span class=\"hljs-keyword\">from</span> evidently.tests <span class=\"hljs-keyword\">import</span> (\n        TestAccuracyScore,\n        TestF1Score,\n        TestColumnDrift,\n    )\n\n    current = pd.read_parquet(test_data.path)\n    reference = pd.read_parquet(baseline_data.path)\n\n    <span class=\"hljs-comment\"># Run data quality and model performance tests</span>\n    suite = TestSuite(tests=[\n        TestAccuracyScore(gte=<span class=\"hljs-number\">0.85</span>),  <span class=\"hljs-comment\"># Must beat threshold</span>\n        TestF1Score(gte=<span class=\"hljs-number\">0.80</span>),\n        TestColumnDrift(column_name=<span class=\"hljs-string\">'prediction'</span>),\n    ])\n\n    suite.run(reference_data=reference, current_data=current)\n    results = suite.as_dict()\n\n    passed = <span class=\"hljs-built_in\">all</span>(test[<span class=\"hljs-string\">'status'</span>] == <span class=\"hljs-string\">'SUCCESS'</span> <span class=\"hljs-keyword\">for</span> test <span class=\"hljs-keyword\">in</span> results[<span class=\"hljs-string\">'tests'</span>])\n    validation_result.log_metric(<span class=\"hljs-string\">'validation_passed'</span>, <span class=\"hljs-built_in\">int</span>(passed))\n\n    <span class=\"hljs-keyword\">return</span> passed\n\n\n<span class=\"hljs-meta\">@dsl.pipeline(<span class=\"hljs-params\">name=<span class=\"hljs-string\">\"ml-training-pipeline\"</span></span>)</span>\n<span class=\"hljs-keyword\">def</span> <span class=\"hljs-title function_\">training_pipeline</span>(<span class=\"hljs-params\">\n    dataset_path: <span class=\"hljs-built_in\">str</span>,\n    hyperparams: <span class=\"hljs-built_in\">dict</span> = {\n        <span class=\"hljs-string\">'learning_rate'</span>: <span class=\"hljs-number\">0.1</span>,\n        <span class=\"hljs-string\">'n_estimators'</span>: <span class=\"hljs-number\">200</span>,\n        <span class=\"hljs-string\">'max_depth'</span>: <span class=\"hljs-number\">5</span>,\n    },\n</span>):\n    <span class=\"hljs-comment\"># Step 1: Prepare data</span>\n    prepare_task = prepare_data(dataset_path=dataset_path)\n\n    <span class=\"hljs-comment\"># Step 2: Train model</span>\n    train_task = train_model(\n        train_data=prepare_task.outputs[<span class=\"hljs-string\">'train_data'</span>],\n        hyperparams=hyperparams,\n    )\n\n    <span class=\"hljs-comment\"># Step 3: Validate model</span>\n    validate_task = validate_model(\n        model=train_task.outputs[<span class=\"hljs-string\">'model_artifact'</span>],\n        test_data=prepare_task.outputs[<span class=\"hljs-string\">'test_data'</span>],\n        baseline_data=prepare_task.outputs[<span class=\"hljs-string\">'baseline_data'</span>],\n    )\n\n    <span class=\"hljs-comment\"># Step 4: Deploy only if validation passes</span>\n    <span class=\"hljs-keyword\">with</span> dsl.Condition(validate_task.output == <span class=\"hljs-literal\">True</span>):\n        deploy_model(\n            model=train_task.outputs[<span class=\"hljs-string\">'model_artifact'</span>],\n            model_name=<span class=\"hljs-string\">\"fraud-detector\"</span>,\n            namespace=<span class=\"hljs-string\">\"ml-serving\"</span>,\n        )\n\n\n<span class=\"hljs-comment\"># Compile and submit</span>\ncompiler.Compiler().<span class=\"hljs-built_in\">compile</span>(training_pipeline, <span class=\"hljs-string\">'pipeline.yaml'</span>)\n</code></pre>\n<h3>Stage 3: Model Serving with Seldon Core</h3>\n<p>Seldon Core provides production-grade model serving with A/B testing and canary deployments:</p>\n<pre role=\"region\" aria-label=\"yaml code block\"><code class=\"hljs language-yaml\" tabindex=\"0\"><span class=\"hljs-comment\"># seldon-deployment.yaml</span>\n<span class=\"hljs-attr\">apiVersion:</span> <span class=\"hljs-string\">machinelearning.seldon.io/v1</span>\n<span class=\"hljs-attr\">kind:</span> <span class=\"hljs-string\">SeldonDeployment</span>\n<span class=\"hljs-attr\">metadata:</span>\n  <span class=\"hljs-attr\">name:</span> <span class=\"hljs-string\">fraud-detector</span>\n  <span class=\"hljs-attr\">namespace:</span> <span class=\"hljs-string\">ml-serving</span>\n<span class=\"hljs-attr\">spec:</span>\n  <span class=\"hljs-attr\">predictors:</span>\n    <span class=\"hljs-comment\"># Canary: New model version (10% traffic)</span>\n    <span class=\"hljs-bullet\">-</span> <span class=\"hljs-attr\">name:</span> <span class=\"hljs-string\">canary</span>\n      <span class=\"hljs-attr\">traffic:</span> <span class=\"hljs-number\">10</span>\n      <span class=\"hljs-attr\">componentSpecs:</span>\n        <span class=\"hljs-bullet\">-</span> <span class=\"hljs-attr\">spec:</span>\n            <span class=\"hljs-attr\">containers:</span>\n              <span class=\"hljs-bullet\">-</span> <span class=\"hljs-attr\">name:</span> <span class=\"hljs-string\">model</span>\n                <span class=\"hljs-attr\">image:</span> <span class=\"hljs-string\">ghcr.io/myorg/fraud-model:v2.1.0</span>\n                <span class=\"hljs-attr\">resources:</span>\n                  <span class=\"hljs-attr\">requests:</span>\n                    <span class=\"hljs-attr\">cpu:</span> <span class=\"hljs-string\">\"2\"</span>\n                    <span class=\"hljs-attr\">memory:</span> <span class=\"hljs-string\">\"4Gi\"</span>\n                  <span class=\"hljs-attr\">limits:</span>\n                    <span class=\"hljs-attr\">cpu:</span> <span class=\"hljs-string\">\"4\"</span>\n                    <span class=\"hljs-attr\">memory:</span> <span class=\"hljs-string\">\"8Gi\"</span>\n      <span class=\"hljs-attr\">graph:</span>\n        <span class=\"hljs-attr\">name:</span> <span class=\"hljs-string\">model</span>\n        <span class=\"hljs-attr\">type:</span> <span class=\"hljs-string\">MODEL</span>\n        <span class=\"hljs-attr\">implementation:</span> <span class=\"hljs-string\">SKLEARN_SERVER</span>\n        <span class=\"hljs-attr\">modelUri:</span> <span class=\"hljs-string\">s3://models/fraud-detector/v2.1.0</span>\n        <span class=\"hljs-attr\">parameters:</span>\n          <span class=\"hljs-bullet\">-</span> <span class=\"hljs-attr\">name:</span> <span class=\"hljs-string\">method</span>\n            <span class=\"hljs-attr\">type:</span> <span class=\"hljs-string\">STRING</span>\n            <span class=\"hljs-attr\">value:</span> <span class=\"hljs-string\">predict_proba</span>\n\n    <span class=\"hljs-comment\"># Default: Current stable version (90% traffic)</span>\n    <span class=\"hljs-bullet\">-</span> <span class=\"hljs-attr\">name:</span> <span class=\"hljs-string\">default</span>\n      <span class=\"hljs-attr\">traffic:</span> <span class=\"hljs-number\">90</span>\n      <span class=\"hljs-attr\">componentSpecs:</span>\n        <span class=\"hljs-bullet\">-</span> <span class=\"hljs-attr\">spec:</span>\n            <span class=\"hljs-attr\">containers:</span>\n              <span class=\"hljs-bullet\">-</span> <span class=\"hljs-attr\">name:</span> <span class=\"hljs-string\">model</span>\n                <span class=\"hljs-attr\">image:</span> <span class=\"hljs-string\">ghcr.io/myorg/fraud-model:v2.0.0</span>\n                <span class=\"hljs-attr\">resources:</span>\n                  <span class=\"hljs-attr\">requests:</span>\n                    <span class=\"hljs-attr\">cpu:</span> <span class=\"hljs-string\">\"2\"</span>\n                    <span class=\"hljs-attr\">memory:</span> <span class=\"hljs-string\">\"4Gi\"</span>\n      <span class=\"hljs-attr\">graph:</span>\n        <span class=\"hljs-attr\">name:</span> <span class=\"hljs-string\">model</span>\n        <span class=\"hljs-attr\">type:</span> <span class=\"hljs-string\">MODEL</span>\n        <span class=\"hljs-attr\">implementation:</span> <span class=\"hljs-string\">SKLEARN_SERVER</span>\n        <span class=\"hljs-attr\">modelUri:</span> <span class=\"hljs-string\">s3://models/fraud-detector/v2.0.0</span>\n\n  <span class=\"hljs-comment\"># A/B test configuration</span>\n  <span class=\"hljs-attr\">annotations:</span>\n    <span class=\"hljs-attr\">seldon.io/ab-test:</span> <span class=\"hljs-string\">\"true\"</span>\n    <span class=\"hljs-attr\">seldon.io/canary-promote-threshold:</span> <span class=\"hljs-string\">\"0.95\"</span> <span class=\"hljs-comment\"># Promote if 95%+ accuracy</span>\n</code></pre>\n<h3>Stage 4: Drift Detection and Auto-Retraining</h3>\n<p>Models degrade over time as data distributions shift. We detect this automatically:</p>\n<pre role=\"region\" aria-label=\"python code block\"><code class=\"hljs language-python\" tabindex=\"0\"><span class=\"hljs-comment\"># monitoring/drift_detector.py</span>\n<span class=\"hljs-keyword\">from</span> evidently.metrics <span class=\"hljs-keyword\">import</span> (\n    DataDriftTable,\n    DatasetDriftMetric,\n    ColumnDriftMetric,\n)\n<span class=\"hljs-keyword\">from</span> evidently.report <span class=\"hljs-keyword\">import</span> Report\n<span class=\"hljs-keyword\">from</span> prometheus_client <span class=\"hljs-keyword\">import</span> Gauge, start_http_server\n<span class=\"hljs-keyword\">import</span> schedule\n<span class=\"hljs-keyword\">import</span> time\n\n<span class=\"hljs-comment\"># Prometheus metrics for Grafana dashboards</span>\ndrift_score = Gauge(\n    <span class=\"hljs-string\">'model_data_drift_score'</span>,\n    <span class=\"hljs-string\">'Current data drift score'</span>,\n    [<span class=\"hljs-string\">'model_name'</span>, <span class=\"hljs-string\">'feature'</span>]\n)\nprediction_drift = Gauge(\n    <span class=\"hljs-string\">'model_prediction_drift'</span>,\n    <span class=\"hljs-string\">'Prediction distribution drift'</span>,\n    [<span class=\"hljs-string\">'model_name'</span>]\n)\n\n<span class=\"hljs-keyword\">def</span> <span class=\"hljs-title function_\">check_drift</span>(<span class=\"hljs-params\">model_name: <span class=\"hljs-built_in\">str</span>, reference_data, current_data</span>):\n    <span class=\"hljs-string\">\"\"\"Run drift detection and expose metrics.\"\"\"</span>\n    report = Report(metrics=[\n        DatasetDriftMetric(),\n        DataDriftTable(),\n    ])\n\n    report.run(\n        reference_data=reference_data,\n        current_data=current_data,\n    )\n\n    results = report.as_dict()\n\n    <span class=\"hljs-comment\"># Update Prometheus metrics</span>\n    dataset_drift = results[<span class=\"hljs-string\">'metrics'</span>][<span class=\"hljs-number\">0</span>][<span class=\"hljs-string\">'result'</span>][<span class=\"hljs-string\">'dataset_drift'</span>]\n    drift_share = results[<span class=\"hljs-string\">'metrics'</span>][<span class=\"hljs-number\">0</span>][<span class=\"hljs-string\">'result'</span>][<span class=\"hljs-string\">'drift_share'</span>]\n\n    prediction_drift.labels(model_name=model_name).<span class=\"hljs-built_in\">set</span>(drift_share)\n\n    <span class=\"hljs-comment\"># Per-feature drift scores</span>\n    <span class=\"hljs-keyword\">for</span> col_name, col_data <span class=\"hljs-keyword\">in</span> results[<span class=\"hljs-string\">'metrics'</span>][<span class=\"hljs-number\">1</span>][<span class=\"hljs-string\">'result'</span>][<span class=\"hljs-string\">'drift_by_columns'</span>].items():\n        drift_score.labels(\n            model_name=model_name,\n            feature=col_name,\n        ).<span class=\"hljs-built_in\">set</span>(col_data[<span class=\"hljs-string\">'drift_score'</span>])\n\n    <span class=\"hljs-comment\"># Trigger retraining if drift exceeds threshold</span>\n    <span class=\"hljs-keyword\">if</span> dataset_drift <span class=\"hljs-keyword\">or</span> drift_share &gt; <span class=\"hljs-number\">0.3</span>:\n        trigger_retraining(model_name)\n\n    <span class=\"hljs-keyword\">return</span> dataset_drift\n\n\n<span class=\"hljs-keyword\">def</span> <span class=\"hljs-title function_\">trigger_retraining</span>(<span class=\"hljs-params\">model_name: <span class=\"hljs-built_in\">str</span></span>):\n    <span class=\"hljs-string\">\"\"\"Trigger Kubeflow pipeline for model retraining.\"\"\"</span>\n    <span class=\"hljs-keyword\">import</span> kfp\n    client = kfp.Client(host=<span class=\"hljs-string\">'http://kubeflow-pipelines:8888'</span>)\n    client.create_run_from_pipeline_package(\n        <span class=\"hljs-string\">'pipeline.yaml'</span>,\n        arguments={<span class=\"hljs-string\">'dataset_path'</span>: <span class=\"hljs-string\">f's3://data/<span class=\"hljs-subst\">{model_name}</span>/latest/'</span>},\n        run_name=<span class=\"hljs-string\">f'auto-retrain-<span class=\"hljs-subst\">{model_name}</span>-<span class=\"hljs-subst\">{<span class=\"hljs-built_in\">int</span>(time.time())}</span>'</span>,\n    )\n\n\n<span class=\"hljs-comment\"># Run drift check every 5 minutes</span>\nschedule.every(<span class=\"hljs-number\">5</span>).minutes.do(\n    check_drift,\n    model_name=<span class=\"hljs-string\">'fraud-detector'</span>,\n    reference_data=load_reference_data(),\n    current_data=load_current_data(),\n)\n</code></pre>\n<h2>GitOps for ML Models</h2>\n<p>Every model deployment is tracked in Git:</p>\n<pre role=\"region\" aria-label=\"yaml code block\"><code class=\"hljs language-yaml\" tabindex=\"0\"><span class=\"hljs-comment\"># argocd/ml-models/fraud-detector/kustomization.yaml</span>\n<span class=\"hljs-attr\">apiVersion:</span> <span class=\"hljs-string\">kustomize.config.k8s.io/v1beta1</span>\n<span class=\"hljs-attr\">kind:</span> <span class=\"hljs-string\">Kustomization</span>\n<span class=\"hljs-attr\">resources:</span>\n  <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">seldon-deployment.yaml</span>\n  <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">hpa.yaml</span>\n  <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">monitoring.yaml</span>\n\n<span class=\"hljs-attr\">images:</span>\n  <span class=\"hljs-bullet\">-</span> <span class=\"hljs-attr\">name:</span> <span class=\"hljs-string\">ghcr.io/myorg/fraud-model</span>\n    <span class=\"hljs-attr\">newTag:</span> <span class=\"hljs-string\">v2.1.0</span> <span class=\"hljs-comment\"># Updated by CI on model promotion</span>\n\n<span class=\"hljs-attr\">patches:</span>\n  <span class=\"hljs-bullet\">-</span> <span class=\"hljs-attr\">target:</span>\n      <span class=\"hljs-attr\">kind:</span> <span class=\"hljs-string\">SeldonDeployment</span>\n      <span class=\"hljs-attr\">name:</span> <span class=\"hljs-string\">fraud-detector</span>\n    <span class=\"hljs-attr\">patch:</span> <span class=\"hljs-string\">|\n      - op: replace\n        path: /spec/predictors/0/traffic\n        value: 10  # Canary percentage, gradually increased\n</span></code></pre>\n<h2>Results</h2>\n<table>\n<thead>\n<tr>\n<th>Metric</th>\n<th>Before MLOps</th>\n<th>After MLOps</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Model deployment time</td>\n<td>3-5 days</td>\n<td>&lt; 1 hour</td>\n</tr>\n<tr>\n<td>Models in production</td>\n<td>3</td>\n<td>15+</td>\n</tr>\n<tr>\n<td>Drift detection time</td>\n<td>Never (manual)</td>\n<td>&lt; 5 minutes</td>\n</tr>\n<tr>\n<td>Experiment reproducibility</td>\n<td>0%</td>\n<td>100%</td>\n</tr>\n<tr>\n<td>A/B test capability</td>\n<td>None</td>\n<td>All models</td>\n</tr>\n<tr>\n<td>Rollback time</td>\n<td>Hours (manual)</td>\n<td>&lt; 2 minutes (GitOps)</td>\n</tr>\n</tbody>\n</table>\n<h2>Lessons Learned</h2>\n<ol>\n<li>\n<p><strong>Version everything</strong>: Data, code, models, and infrastructure. If you can't reproduce it, you can't debug it.</p>\n</li>\n<li>\n<p><strong>Separate model training from serving</strong>: Training is batch-oriented and GPU-heavy. Serving is latency-sensitive and CPU-heavy. Different scaling patterns require different infrastructure.</p>\n</li>\n<li>\n<p><strong>Drift detection is non-negotiable</strong>: A model that worked great at deployment can silently degrade. Statistical tests on incoming data distributions are your early warning system.</p>\n</li>\n<li>\n<p><strong>Canary deployments for models</strong>: Never switch 100% traffic to a new model version. Start at 5-10%, monitor key metrics, and gradually promote.</p>\n</li>\n<li>\n<p><strong>Treat ML infrastructure as a platform</strong>: Data scientists shouldn't need to know Kubernetes. Provide simple abstractions (submit training job, deploy model, check metrics) that hide infrastructure complexity.</p>\n</li>\n</ol>\n<h2>The MLOps Maturity Roadmap</h2>\n<p>Start here and progress iteratively:</p>\n<ol>\n<li><strong>Foundation</strong> (Month 1-2): MLflow for experiment tracking, DVC for data versioning</li>\n<li><strong>Automation</strong> (Month 3-4): Kubeflow pipelines for training, model registry for promotion</li>\n<li><strong>Serving</strong> (Month 5-6): Seldon Core with canary deployments and A/B testing</li>\n<li><strong>Monitoring</strong> (Month 7+): Drift detection, auto-retraining, SLOs for model performance</li>\n</ol>\n<p>The goal isn't to automate everything at once — it's to systematically remove the friction between \"model works in notebook\" and \"model works in production.\"</p>"}