🎉 75% of content is free forever — Unlock Premium from $10/mo →
CW
NEWSLIVESearch All Content
đŸ’ŧ Servicesâ„šī¸ Aboutâœ‰ī¸ ContactView Pricing Plansfrom $10

Production MLOps Pipeline (Kubeflow + Seldon + Prometheus)

AI/ML ProjectsMLOps⭐ Premium

Advertisement

Production MLOps Pipeline

Kubeflow + Seldon + Prometheus | Enterprise ML Infrastructure

Expert20+ HoursKubernetes Required

What You'll Learn

  • 1Compose a Kubeflow pipeline whose validation gate blocks a weak model before serving
  • 2Size canary exposure from a sample calculation instead of a hunch
  • 3Pin artifacts by digest so rollback restores the exact binary that failed
  • 4Attach runbooks to Prometheus rules so every alert has a documented first action

Project Overview

Problem Statement

Deploying ML models to production requires more than just model code. Teams need CI/CD pipelines for models, automated testing, canary deployments, rollback mechanisms, and infrastructure monitoring. This project builds a complete MLOps platform using Kubernetes-native tools.

Objectives

  • Build end-to-end ML pipeline with Kubeflow Pipelines
  • Deploy models with Seldon Core for A/B testing and canary releases
  • Implement model versioning and registry
  • Create CI/CD pipelines for ML with GitHub Actions
  • Set up comprehensive monitoring with Prometheus + Grafana
ComponentTechnology
Pipeline OrchestrationKubeflow Pipelines
Model ServingSeldon Core + Triton
Container OrchestrationKubernetes (EKS/GKE)
CI/CDGitHub Actions + ArgoCD
Model RegistryMLflow + S3
MonitoringPrometheus + Grafana
Feature StoreFeast on Kubernetes
SecretsHashiCorp Vault

Design Rationale

The platform exists to make a bad deploy cheap. Immutable artifacts, a validation gate, canary traffic, and automated rollback all reduce the same quantity: the expected cost of a change, which is the probability it breaks times its blast radius times how long it stays broken:

Cutting mean time to restore from 60 minutes to 5 minutes through automatic rollback reduces expected cost twelve-fold without changing how often anyone deploys, which is why the metrics table tracks rollback time and pipeline success rate next to model accuracy.

Canary analysis is a sample size problem, not an opinion. At 5 percent of traffic a model serving 20 requests per second puts 1 request per second on the canary, and detecting an error rate move from 1 percent to 3 percent requires:

With 95 percent confidence, 80 percent power, , and , that is about 950 canary requests, or roughly 16 minutes of exposure. The rollout steps of 5, then 25, then 100 percent therefore hold 30 minutes at each stage rather than 5, and the blast radius never exceeds a quarter of production.

Cost per prediction is a unit conversion teams skip until the bill arrives. Four replicas on instances billed at 0.50 USD per hour, serving 30 requests per second, cost:

That is 0.24 USD per thousand predictions before model storage and egress, and it is the number that decides between on-demand and spot instances, between naive and batched inference, and between an 8B and a 7B model.

Rollback speed is also availability arithmetic:

With a weekly deploy cadence, MTBF is 10,080 minutes; a 5 minute rollback yields 0.9995, or 99.95 percent availability, while a 60 minute manual recovery yields 0.994. Same code, same failure rate, an order of magnitude different outcome.

Architecture Diagram

Production MLOps ArchitectureGit Push(Code/Data)GitHubActions CIKubeflowPipeline RunModelRegistrySeldon CoreServingCanary Deploy5% - 25% - 100%PrometheusMetricsGrafanaDashboardAlertManager(PagerDuty)

Step-by-Step Implementation

Step 1: Kubernetes Cluster Setup

# Install required tools
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# Create cluster (example with kind for local dev)
kind create cluster --name ml-cluster --config kind-config.yaml

# Install Kubeflow Pipelines
export PIPELINE_VERSION=2.0.0
kubectl apply -k "github.com/kubeflow/pipelines/manifests/kustomize/env/dev?ref=$PIPELINE_VERSION"

# Install Seldon Core
kubectl create namespace seldon-system || true
helm install seldon-core seldon-core-operator -n seldon-system \
  --set usageMetrics.enabled=true \
  --set istio.enabled=true

# Install Prometheus + Grafana
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring --create-namespace

Step 2: Kubeflow Pipeline Definition

Define an ML training pipeline as a Kubeflow Pipeline with reusable components.

python
# pipelines/training_pipeline.py
from kfp import dsl
from kfp.components import load_component_from_file


@dsl.component(base_image="python:3.10-slim")
def preprocess_data(input_path: str, output_path: str):
    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import StandardScaler
    import joblib
    import os

    df = pd.read_csv(input_path)
    scaler = StandardScaler()
    feature_cols = [c for c in df.columns if c != "target"]
    df[feature_cols] = scaler.fit_transform(df[feature_cols])
    train_df, test_df = train_test_split(df, test_size=0.2, random_state=42)

    os.makedirs(output_path, exist_ok=True)
    train_df.to_csv(f"{output_path}/train.csv", index=False)
    test_df.to_csv(f"{output_path}/test.csv", index=False)
    joblib.dump(scaler, f"{output_path}/scaler.joblib")


@dsl.component(base_image="python:3.10-slim")
def train_model(data_path: str, model_path: str, params: dict):
    import pandas as pd
    import lightgbm as lgb
    import mlflow
    import json
    import os

    train_df = pd.read_csv(f"{data_path}/train.csv")
    X_train = train_df.drop("target", axis=1)
    y_train = train_df["target"]

    mlflow.set_experiment("production-training")
    with mlflow.start_run():
        mlflow.log_params(params)

        train_data = lgb.Dataset(X_train, label=y_train)
        model = lgb.train(
            params, train_data, num_boost_round=500,
            valid_sets=[train_data], callbacks=[lgb.log_evaluation(50)],
        )

        mlflow.lightgbm.log_model(model, "model")
        os.makedirs(model_path, exist_ok=True)
        model.save_model(f"{model_path}/model.txt")
        mlflow.log_artifact(f"{model_path}/model.txt")


@dsl.component(base_image="python:3.10-slim")
def evaluate_model(data_path: str, model_path: str, metrics_path: str):
    import pandas as pd
    import lightgbm as lgb
    from sklearn.metrics import accuracy_score, f1_score, roc_auc_score
    import json
    import os

    test_df = pd.read_csv(f"{data_path}/test.csv")
    X_test = test_df.drop("target", axis=1)
    y_test = test_df["target"]

    model = lgb.Booster(model_file=f"{model_path}/model.txt")
    y_pred = (model.predict(X_test) > 0.5).astype(int)

    metrics = {
        "accuracy": float(accuracy_score(y_test, y_pred)),
        "f1": float(f1_score(y_test, y_pred)),
        "auc": float(roc_auc_score(y_test, model.predict(X_test))),
    }

    os.makedirs(metrics_path, exist_ok=True)
    with open(f"{metrics_path}/metrics.json", "w") as f:
        json.dump(metrics, f, indent=2)
    print(f"Metrics: {metrics}")


@dsl.component(base_image="python:3.10-slim")
def validate_model(metrics_path: str, threshold: float = 0.85):
    import json
    with open(f"{metrics_path}/metrics.json") as f:
        metrics = json.load(f)
    assert metrics["accuracy"] >= threshold, f"Model accuracy {metrics['accuracy']} below threshold {threshold}"
    print(f"Model validation passed: accuracy={metrics['accuracy']:.4f}")


@dsl.pipeline(
    name="training-pipeline",
    description="End-to-end ML training pipeline",
)
def training_pipeline(
    input_path: str = "gs://ml-data/input/",
    output_path: str = "gs://ml-data/processed/",
    model_path: str = "gs://ml-models/production/",
    metrics_path: str = "gs://ml-metrics/",
):
    preprocess_task = preprocess_data(input_path=input_path, output_path=output_path)

    train_task = train_model(
        data_path=preprocess_task.outputs["output_path"],
        model_path=model_path,
        params={"num_leaves": 31, "learning_rate": 0.05, "objective": "binary"},
    )

    eval_task = evaluate_model(
        data_path=preprocess_task.outputs["output_path"],
        model_path=train_task.outputs["model_path"],
        metrics_path=metrics_path,
    )

    validate_task = validate_model(metrics_path=eval_task.outputs["metrics_path"])

    # Set dependency
    validate_task.after(train_task)

Step 3: Seldon Core Model Deployment

Deploy models with Seldon Core for A/B testing, canary releases, and monitoring.

python
# seldon/model.py
import numpy as np
import lightgbm as lgb
import logging
from typing import Dict, List

logger = logging.getLogger(__name__)


class SeldonModel:
    def __init__(self):
        self.model = None
        self.scaler = None

    def load(self, model_path: str):
        self.model = lgb.Booster(model_file=f"{model_path}/model.txt")
        logger.info(f"Loaded model from {model_path}")

    def predict(self, X: np.ndarray, features_names: List[str] = None) -> np.ndarray:
        """Seldon predict interface."""
        predictions = self.model.predict(X)
        return (predictions > 0.5).astype(int).reshape(-1, 1)

    def predict_proba(self, X: np.ndarray, features_names: List[str] = None) -> np.ndarray:
        """Return prediction probabilities."""
        proba = self.model.predict(X)
        return np.column_stack([1 - proba, proba])

    def feedback(self, feedback_request) -> bool:
        """Handle feedback for online learning."""
        logger.info(f"Received feedback: {feedback_request}")
        return True

Seldon Deployment YAML

# seldon/deployment.yaml
apiVersion: machinelearning.seldon.io/v1
kind: SeldonDeployment
metadata:
  name: ml-model-production
  namespace: ml-serving
spec:
  predictors:
    - name: stable
      replicas: 3
      traffic: 90
      graph:
        name: model
        implementation: UNKNOWN_IMPLEMENTATION
        modelUri: gs://ml-models/production/v1.0.0
        children: []
      componentSpecs:
        - spec:
            containers:
              - name: model
                image: ml-model:latest
                resources:
                  requests:
                    cpu: "1"
                    memory: "2Gi"
                  limits:
                    cpu: "2"
                    memory: "4Gi"
    - name: canary
      replicas: 1
      traffic: 10
      graph:
        name: model
        implementation: UNKNOWN_IMPLEMENTATION
        modelUri: gs://ml-models/production/v1.1.0
        children: []
      componentSpecs:
        - spec:
            containers:
              - name: model
                image: ml-model:canary

Step 4: CI/CD Pipeline with GitHub Actions

# .github/workflows/ml-pipeline.yaml
name: ML Pipeline CI/CD

on:
  push:
    branches: [main]
    paths:
      - "src/**"
      - "pipelines/**"
      - "seldon/**"

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.10"
      - run: pip install -r requirements.txt
      - run: pytest tests/ -v --cov=src

  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}

  deploy-staging:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: kubectl apply -f seldon/deployment.yaml -n staging
      - run: kubectl rollout status seldondeployment/ml-model-staging -n staging

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - run: kubectl apply -f seldon/deployment.yaml -n production
      - run: kubectl rollout status seldondeployment/ml-model-production -n production

Step 5: Prometheus Monitoring Rules

# monitoring/prometheus-rules.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: ml-model-alerts
  namespace: monitoring
spec:
  groups:
    - name: ml-model
      rules:
        - alert: HighPredictionLatency
          expr: histogram_quantile(0.99, rate(seldon_prediction_duration_seconds_bucket[5m])) > 0.5
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: "Model prediction latency is high"
            description: "P99 latency is {{ $value }}s (threshold: 0.5s)"

        - alert: HighErrorRate
          expr: rate(seldon_prediction_errors_total[5m]) / rate(seldon_predictions_total[5m]) > 0.05
          for: 2m
          labels:
            severity: critical
          annotations:
            summary: "Model error rate exceeds 5%"

        - alert: LowAccuracy
          expr: ml_model_accuracy < 0.85
          for: 30m
          labels:
            severity: warning
          annotations:
            summary: "Model accuracy has dropped below threshold"

        - alert: DataDriftDetected
          expr: ml_data_drift_score > 0.1
          for: 15m
          labels:
            severity: warning
          annotations:
            summary: "Data drift detected in production features"

Step 6: Feature Store with Feast

python
# feature_store/feature_definitions.py
from feast import FeatureStore, Entity, Feature, ValueType
from feast import FileSource, FeatureView
from datetime import timedelta
import pandas as pd


# Define entities
user_entity = Entity(name="user_id", value_type=ValueType.INT64, description="User ID")
product_entity = Entity(name="product_id", value_type=ValueType.INT64, description="Product ID")


# Define feature views
user_features = FeatureView(
    name="user_features",
    entities=["user_id"],
    ttl=timedelta(days=1),
    features=[
        Feature(name="purchase_count_7d", value_type=ValueType.INT64),
        Feature(name="avg_order_value", value_type=ValueType.FLOAT),
        Feature(name="last_purchase_days_ago", value_type=ValueType.INT64),
    ],
    online=True,
    source=FileSource(path="data/user_features.parquet", timestamp_field="event_timestamp"),
)

product_features = FeatureView(
    name="product_features",
    entities=["product_id"],
    ttl=timedelta(days=7),
    features=[
        Feature(name="price", value_type=ValueType.FLOAT),
        Feature(name="category_id", value_type=ValueType.INT64),
        Feature(name="avg_rating", value_type=ValueType.FLOAT),
        Feature(name="review_count", value_type=ValueType.INT64),
    ],
    online=True,
    source=FileSource(path="data/product_features.parquet", timestamp_field="event_timestamp"),
)


# Feature retrieval
store = FeatureStore(repo_path=".")

training_df = store.get_historical_features(
    entity_df=entity_df,
    features=["user_features:purchase_count_7d", "product_features:price"],
).to_df()

online_features = store.get_online_features(
    features=["user_features:purchase_count_7d", "product_features:price"],
    entity_rows=[{"user_id": 1, "product_id": 42}],
).to_dict()

â„šī¸

Implement progressive rollouts for model deployments: start with 5% traffic, monitor for 30 minutes, then gradually increase to 100%. Set automatic rollback triggers on error rate spikes.

💡

Use ArgoCD for GitOps-based deployment. Store all Kubernetes manifests in Git and let ArgoCD sync the desired state. This provides audit trails and easy rollbacks.

Performance Metrics

MetricTargetDescription
Deployment FrequencyDailyModel deployments per day
Lead Time< 1hrCode commit to production
Rollback Time< 5minTime to rollback bad deploy
Pipeline Success Rate> 95%CI/CD pipeline reliability
Model Serving Latency< 100msP99 inference latency
Infrastructure CostOptimizedPer-prediction cost

What Breaks in Production

MLOps failures come from the seams between tools. The most expensive is training and serving skew: the pipeline fits a StandardScaler at train time while the service recomputes normalization from live batches, so scores drift with no code change anywhere. Ship the fitted preprocessor inside the model artifact and load both as one unit.

Second, mutable tags. A latest tag, or a v1.1.0 tag that gets overwritten after a hotfix, means a rollback can restore a different artifact than the one that failed. Pin images by digest, record that digest in the MLflow run, and let rollbacks resolve digests instead of names.

Third, missing readiness probes. A pod that answers 200 before the model finishes loading receives traffic it cannot serve and produces a burst of 503s at every deploy. Readiness must verify model load, and the PodDisruptionBudget should keep at least two of three replicas available while a node drains.

Fourth, alerts without runbooks. HighPredictionLatency firing at 3 a.m. with no documented first action costs exactly as much MTTR as having no alert at all. Every rule ships with a link to its runbook and the one-line rollback command.

Finally, gates that are too loose. A validate_model step with an absolute threshold of 0.85 happily passes a model at 0.851 that is four points below the incumbent, and the regression reaches production with a green pipeline. Gate against the incumbent by a margin, not against a floor.

Interview Talking Points

  1. Kubeflow Pipelines: Provides reproducible, version-controlled ML pipelines on Kubernetes. Each pipeline run creates immutable artifacts.
  2. Seldon Core: Kubernetes-native model serving with built-in A/B testing, canary deployments, and explainability.
  3. Feature Store: Feast provides a central repository for feature definitions, ensuring consistency between training and serving.
  4. GitOps: ArgoCD + GitHub Actions provides automated, auditable deployments with automatic rollback on failures.
  5. Monitoring: Prometheus + Grafana provides infrastructure and model-specific monitoring with configurable alerting.
  6. Cost Optimization: Spot instances for training, auto-scaling for serving, and resource quotas for cost control.

âš ī¸

Kubeflow has a steep learning curve. Start with simple pipelines and gradually add complexity. Ensure your team is comfortable with Kubernetes before adopting the full platform.

â„šī¸

For teams not ready for Kubernetes, consider managed ML platforms like AWS SageMaker, GCP Vertex AI, or Azure ML which provide similar capabilities with less operational overhead.

Common Mistakes

Fitting preprocessing at train time and recomputing it at serve time

Fix: Serialize the fitted scaler and feature code inside the model artifact and load them as one unit.

Tagging images latest and overwriting version tags

Fix: Pin by digest, record the digest in MLflow, and let rollback resolve digests rather than names.

Gating on an absolute accuracy floor

Fix: Gate against the incumbent by a margin so a model barely above the floor cannot ship a regression.

Best Practices

  • Hold each canary step at least 15 minutes so the sample reaches statistical significance
  • Store every Kubernetes manifest in Git and let ArgoCD sync, so rollback is a git revert
  • Attach a runbook link and rollback command to every Prometheus alert rule

Key Takeaways

  • Expected change cost is P(fail) times blast radius times MTTR, so faster rollback cuts risk without slowing deploys
  • Detecting an error rate move from 1 to 3 percent needs about 950 canary requests, roughly 16 minutes at 5 percent traffic
  • Four replicas at 0.50 USD per hour serving 30 QPS cost 0.24 USD per thousand predictions
  • Availability equals MTBF over MTBF plus MTTR, so a 5 minute rollback turns weekly failures into 99.95 percent uptime

Frequently Asked Questions

Kubeflow or a managed platform?

Kubeflow wins when you already run Kubernetes, need custom containers, and have a platform team to own upgrades. A managed service wins when ML is not the core competency: you trade roughly 30 percent of the flexibility for the operational burden disappearing. Most teams are happier starting managed and justifying Kubernetes only after a concrete limit is hit.

Canary or blue-green for models?

Canary, because models degrade continuously rather than failing outright, and only a traffic split reveals a quality regression before every user sees it. Blue-green suits stateless services with instant health checks; a model needs minutes of representative traffic, which is why the schedule holds each step for 30 minutes.

How often should the pipeline retrain?

On drift or accuracy alerts first, with a scheduled path as a backstop at 30 days. A fixed weekly retrain burns compute when nothing changed and still reacts too slowly when something changes on Tuesday, so evidence-triggered runs with a maximum age give the best cost-to-freshness ratio.

Interview Questions

Q1: Explain progressive delivery for a model as opposed to a normal service.

A normal service has a health check; a model needs a quality check on real traffic. So you route 5 percent to the new weights, wait about 16 minutes for 950 requests, compare error and latency against the incumbent, then step to 25 and 100 percent with automatic rollback on breach.

Q2: How do you guarantee training and serving compute identical features?

Package the fitted preprocessor with the model in one artifact, add a golden-row test that asserts identical scores offline and online, and refuse deploys when the parity check fails. Skew is almost always a recomputed statistic, so serialization removes the whole class.

Q3: Walk me through your rollback procedure.

ArgoCD reverts the manifest to the previous digest, Kubernetes rolls the Deployment, readiness gates traffic until the old model loads, and the canary percentage returns to zero. Target under 5 minutes, which holds availability at 99.95 percent for a weekly deploy cadence.

🔒

Premium Content

Production MLOps Pipeline (Kubeflow + Seldon + Prometheus)

You've previewed the first section. Unlock this full lesson and 900+ advanced tutorials with a Premium plan.

đŸŽ¯End-to-end Projects
đŸ’ŧInterview Prep
📜Certificates
🤝Community Access

Already a member? Log in

—
☆☆☆☆☆
0 ratings

Rate & Feedback

Advertisement