Wednesday, April 22, 2026

MLOps on OCI: Applying a DBA Mindset to Machine Learning

MLOps is the discipline of treating machine learning systems the way seasoned DBAs treat database pipelines: with version control, monitoring, rollback capability, and zero tolerance for silent failures. This post walks through a production-grade MLOps architecture on Oracle Cloud Infrastructure (OCI), complete with working scripts, pipeline definitions, and the hard-won lessons that don't show up in vendor documentation.

The architecture covers three major phases:

  1. Data Preparation — exploration, cleansing, feature engineering in OCI Data Science
  2. Model Training — automated CI/CD pipeline via OCI DevOps, containerized jobs, MLFlow tracking
  3. Model Deployment — serving via OKE (Oracle Kubernetes Engine) with a live inference endpoint

Let's build it layer by layer.


Architecture Overview





GitHub ──(code push trigger)──► OCI DevOps Build & Deploy Pipeline
                                       │
              ┌────────────────────────┼────────────────────────┐
              ▼                        ▼                        ▼
   OCI DataScience             OCI Container Registry        OCI ObjectStorage
  ┌──────────────────┐      ┌──────────────────────┐      ┌──────────────────┐
  │ Data Access &    │      │ Training Images       │      │ Datasets         │
  │ Exploration      │      │ Inference Images      │      │ Model Artifacts  │
  │ Model Training   │      └──────────────────────┘      │ Model Backups    │
  │ Model Dev        │                                     └──────────────────┘
  └──────────────────┘
              │
              └──────────────────────────────────────────────────►
                                                              OKE / MLFlow
                                                    ┌─────────────────────────┐
                                                    │ Model Evaluation        │
                                                    │ Model Monitoring        │
                                                    │ Model Registry          │
                                                    │ Model Inference Endpoint│
                                                    └─────────────────────────┘


The bottom layer — OCI IAM, Logging, Monitoring, Secrets, VCN, Notifications — is your operational safety net that runs silently under everything else.


Phase 1 — Data Preparation in OCI Data Science

1.1 Environment Setup

Every project starts with a reproducible conda environment. Don't skip this — environment drift is the #1 cause of "but it worked on my machine" incidents.

# environment.yml
name: mlops-oci
channels:
  - conda-forge
  - defaults
dependencies:
  - python=3.10
  - pip
  - pip:
    - oracle-ads==2.9.1
    - scikit-learn==1.3.0
    - pandas==2.0.3
    - mlflow==2.8.1
    - oci==2.112.0
    - boto3
    - pyarrow
    - great-expectations==0.18.0
conda env create -f environment.yml
conda activate mlops-oci

1.2 Data Access & Exploration (OCI ObjectStorage → Notebook)

# data_exploration.py
import ads
import pandas as pd
from ads.dataset.factory import DatasetFactory
from great_expectations.core import ExpectationSuite

# Authenticate using resource principal (preferred in OCI notebooks)
ads.set_auth(auth="resource_principal")

BUCKET       = "mlops-datasets"
NAMESPACE    = "your-tenancy-namespace"
OBJECT_NAME  = "churn_raw.parquet"

# Pull dataset from Object Storage
ds = DatasetFactory.open(
    f"oci://{BUCKET}@{NAMESPACE}/{OBJECT_NAME}",
    format="parquet",
    target="churn"
)

df = ds.to_pandas()
print(f"Shape: {df.shape}")
print(df.dtypes)
print(df.describe())

1.3 Data Quality Gate with Great Expectations

This is the DBA mindset applied to ML: validate your data before you trust it.

# data_quality.py
import great_expectations as gx

context = gx.get_context()

# Define expectations
suite = context.add_expectation_suite("churn_suite")

validator = context.get_validator(
    batch_request=...,
    expectation_suite_name="churn_suite"
)

# Column presence
validator.expect_table_columns_to_match_ordered_list(
    column_list=["customer_id","tenure","monthly_charges","total_charges","churn"]
)

# Nullability
validator.expect_column_values_to_not_be_null("customer_id")
validator.expect_column_values_to_not_be_null("churn")

# Value ranges
validator.expect_column_values_to_be_between("tenure", min_value=0, max_value=120)
validator.expect_column_values_to_be_between("monthly_charges", min_value=0, max_value=500)

# Categorical validity
validator.expect_column_values_to_be_in_set("churn", value_set=["Yes", "No"])

results = validator.validate()

if not results["success"]:
    raise ValueError(f"Data quality check FAILED:\n{results}")

print("✅ Data quality checks passed.")

1.4 Feature Engineering & Save to Object Storage

# feature_engineering.py
import pandas as pd
import ads
from ads.dataset.factory import DatasetFactory

ads.set_auth(auth="resource_principal")

def engineer_features(df: pd.DataFrame) -> pd.DataFrame:
    df = df.copy()

    # Encode target
    df["churn_flag"] = (df["churn"] == "Yes").astype(int)

    # Derived features
    df["avg_monthly_spend"] = df["total_charges"] / (df["tenure"] + 1)
    df["is_long_term"]      = (df["tenure"] > 24).astype(int)
    df["high_spender"]      = (df["monthly_charges"] > 70).astype(int)

    # One-hot encode
    cat_cols = ["contract", "payment_method", "internet_service"]
    df = pd.get_dummies(df, columns=cat_cols, drop_first=True)

    # Drop originals
    df.drop(columns=["churn", "customer_id"], inplace=True)

    return df

df_raw        = pd.read_parquet("churn_raw.parquet")
df_features   = engineer_features(df_raw)

# Save engineered features back to Object Storage
output_path   = "oci://mlops-datasets@your-tenancy-namespace/churn_features.parquet"
df_features.to_parquet(output_path)

print(f"✅ Feature set saved: {df_features.shape}")

Phase 2 — Model Training with OCI DevOps CI/CD

2.1 Project Structure

mlops-churn/
├── .github/                    # GitHub metadata (PR templates, etc.)
├── build_spec.yaml             # OCI DevOps build spec
├── deploy_spec.yaml            # OCI DevOps deploy spec
├── Dockerfile.train            # Training container
├── Dockerfile.inference        # Inference container
├── src/
│   ├── train.py                # Training entrypoint
│   ├── evaluate.py             # Evaluation logic
│   ├── serve.py                # FastAPI inference server
│   └── utils/
│       ├── data_quality.py
│       └── feature_engineering.py
├── k8s/
│   ├── deployment.yaml         # OKE deployment manifest
│   └── service.yaml            # OKE service manifest
├── environment.yml
└── requirements.txt

2.2 OCI DevOps Build Spec

# build_spec.yaml
version: 0.1
component: build
timeoutInSeconds: 1800
shell: bash

steps:
  - type: Command
    name: "Install dependencies"
    command: |
      pip install -r requirements.txt

  - type: Command
    name: "Run data quality checks"
    command: |
      python src/utils/data_quality.py

  - type: Command
    name: "Build Training Docker Image"
    command: |
      docker build \
        -t ${OCI_RESOURCE_PRINCIPAL_REGION}.ocir.io/${TENANCY_NAMESPACE}/mlops-train:${OCI_BUILD_RUN_ID} \
        -f Dockerfile.train .

  - type: Command
    name: "Push Training Image to OCIR"
    command: |
      docker push \
        ${OCI_RESOURCE_PRINCIPAL_REGION}.ocir.io/${TENANCY_NAMESPACE}/mlops-train:${OCI_BUILD_RUN_ID}

  - type: Command
    name: "Build Inference Docker Image"
    command: |
      docker build \
        -t ${OCI_RESOURCE_PRINCIPAL_REGION}.ocir.io/${TENANCY_NAMESPACE}/mlops-infer:${OCI_BUILD_RUN_ID} \
        -f Dockerfile.inference .

  - type: Command
    name: "Push Inference Image to OCIR"
    command: |
      docker push \
        ${OCI_RESOURCE_PRINCIPAL_REGION}.ocir.io/${TENANCY_NAMESPACE}/mlops-infer:${OCI_BUILD_RUN_ID}

outputArtifacts:
  - name: train_image_tag
    type: BINARY
    location: ${OCI_BUILD_RUN_ID}

2.3 Training Script with MLFlow Tracking

# src/train.py
import os
import mlflow
import mlflow.sklearn
import pandas as pd
import numpy as np
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import (
    classification_report, roc_auc_score,
    precision_score, recall_score, f1_score
)
import ads
import joblib

# ── Config ────────────────────────────────────────────────────────────────────
ads.set_auth(auth="resource_principal")

MLFLOW_TRACKING_URI = os.environ["MLFLOW_TRACKING_URI"]
EXPERIMENT_NAME     = "churn-prediction-v2"
BUCKET              = "mlops-datasets"
NAMESPACE           = os.environ["OCI_TENANCY_NAMESPACE"]
MODEL_BUCKET        = "mlops-artifacts"

mlflow.set_tracking_uri(MLFLOW_TRACKING_URI)
mlflow.set_experiment(EXPERIMENT_NAME)

# ── Load Features ─────────────────────────────────────────────────────────────
df = pd.read_parquet(
    f"oci://{BUCKET}@{NAMESPACE}/churn_features.parquet"
)

X = df.drop(columns=["churn_flag"])
y = df["churn_flag"]

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# ── Hyperparameters ───────────────────────────────────────────────────────────
params = {
    "n_estimators":   int(os.environ.get("N_ESTIMATORS", 200)),
    "max_depth":      int(os.environ.get("MAX_DEPTH",    5)),
    "learning_rate":  float(os.environ.get("LEARNING_RATE", 0.05)),
    "subsample":      float(os.environ.get("SUBSAMPLE",   0.8)),
    "random_state":   42,
}

# ── Train & Track ─────────────────────────────────────────────────────────────
with mlflow.start_run() as run:
    mlflow.log_params(params)
    mlflow.set_tag("model_type", "GradientBoosting")
    mlflow.set_tag("dataset_version", "churn_features_v2")

    model = GradientBoostingClassifier(**params)
    model.fit(X_train, y_train)

    # Metrics
    y_pred  = model.predict(X_test)
    y_proba = model.predict_proba(X_test)[:, 1]

    metrics = {
        "roc_auc":   roc_auc_score(y_test, y_proba),
        "precision": precision_score(y_test, y_pred),
        "recall":    recall_score(y_test, y_pred),
        "f1_score":  f1_score(y_test, y_pred),
    }

    # Cross-validation stability check
    cv_scores = cross_val_score(model, X_train, y_train, cv=5, scoring="roc_auc")
    metrics["cv_roc_auc_mean"] = cv_scores.mean()
    metrics["cv_roc_auc_std"]  = cv_scores.std()

    mlflow.log_metrics(metrics)
    print(classification_report(y_test, y_pred))
    print(f"\nšŸ“Š Metrics: {metrics}")

    # ── Promotion Gate ─────────────────────────────────────────────────────────
    MIN_AUC = float(os.environ.get("MIN_AUC_THRESHOLD", 0.80))
    if metrics["roc_auc"] < MIN_AUC:
        raise ValueError(
            f"❌ Model ROC-AUC {metrics['roc_auc']:.4f} < threshold {MIN_AUC}. "
            "Halting promotion."
        )

    # ── Log Model & Register ───────────────────────────────────────────────────
    mlflow.sklearn.log_model(
        model,
        artifact_path="model",
        registered_model_name="churn-gbt",
        input_example=X_test.head(5),
        signature=mlflow.models.infer_signature(X_test, y_pred),
    )

    # Save artifact to Object Storage as backup
    local_path = "/tmp/model.joblib"
    joblib.dump(model, local_path)

    import oci
    object_storage = oci.object_storage.ObjectStorageClient(
        config=oci.config.from_file()
    )
    run_id = run.info.run_id
    with open(local_path, "rb") as f:
        object_storage.put_object(
            namespace_name=NAMESPACE,
            bucket_name=MODEL_BUCKET,
            object_name=f"churn-gbt/{run_id}/model.joblib",
            put_object_body=f,
        )

    print(f"\n✅ Run ID: {run_id}")
    print(f"✅ Model registered in MLFlow: churn-gbt")
    print(f"✅ Artifact backed up to OCI Object Storage")

2.4 Dockerfiles

# Dockerfile.train
FROM python:3.10-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY src/ ./src/
COPY environment.yml .

ENV PYTHONUNBUFFERED=1

ENTRYPOINT ["python", "src/train.py"]
# Dockerfile.inference
FROM python:3.10-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt fastapi uvicorn

COPY src/serve.py .

EXPOSE 8080
ENTRYPOINT ["uvicorn", "serve:app", "--host", "0.0.0.0", "--port", "8080"]

Phase 3 — Model Serving on OKE with FastAPI

3.1 Inference Server

# src/serve.py
import os
import mlflow
import mlflow.sklearn
import pandas as pd
import numpy as np
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from typing import List, Optional
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = FastAPI(
    title="Churn Prediction API",
    description="MLOps inference endpoint — OCI OKE",
    version="2.0.0"
)

# ── Model Loading ──────────────────────────────────────────────────────────────
MLFLOW_TRACKING_URI    = os.environ["MLFLOW_TRACKING_URI"]
MODEL_NAME             = os.environ.get("MODEL_NAME", "churn-gbt")
MODEL_STAGE            = os.environ.get("MODEL_STAGE", "Production")

mlflow.set_tracking_uri(MLFLOW_TRACKING_URI)

logger.info(f"Loading model: {MODEL_NAME} @ stage={MODEL_STAGE}")
model = mlflow.sklearn.load_model(f"models:/{MODEL_NAME}/{MODEL_STAGE}")
logger.info("✅ Model loaded successfully")

# ── Schemas ────────────────────────────────────────────────────────────────────
class CustomerFeatures(BaseModel):
    tenure:             float = Field(..., ge=0, le=120)
    monthly_charges:    float = Field(..., ge=0, le=500)
    avg_monthly_spend:  float
    is_long_term:       int   = Field(..., ge=0, le=1)
    high_spender:       int   = Field(..., ge=0, le=1)
    contract_one_year:  int   = Field(0, ge=0, le=1)
    contract_two_year:  int   = Field(0, ge=0, le=1)

class PredictionRequest(BaseModel):
    customers: List[CustomerFeatures]
    threshold: Optional[float] = 0.5

class PredictionResult(BaseModel):
    customer_index:   int
    churn_probability: float
    churn_prediction:  bool
    risk_tier:         str

# ── Endpoints ──────────────────────────────────────────────────────────────────
@app.get("/health")
def health():
    return {"status": "ok", "model": MODEL_NAME, "stage": MODEL_STAGE}

@app.post("/predict", response_model=List[PredictionResult])
def predict(request: PredictionRequest):
    try:
        df = pd.DataFrame([c.dict() for c in request.customers])
        probas = model.predict_proba(df)[:, 1]

        results = []
        for i, prob in enumerate(probas):
            tier = (
                "HIGH"   if prob >= 0.70 else
                "MEDIUM" if prob >= 0.40 else
                "LOW"
            )
            results.append(PredictionResult(
                customer_index    = i,
                churn_probability = round(float(prob), 4),
                churn_prediction  = bool(prob >= request.threshold),
                risk_tier         = tier,
            ))

        return results

    except Exception as e:
        logger.error(f"Prediction error: {e}")
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/model/info")
def model_info():
    client = mlflow.tracking.MlflowClient()
    mv     = client.get_latest_versions(MODEL_NAME, stages=[MODEL_STAGE])[0]
    return {
        "name":        mv.name,
        "version":     mv.version,
        "stage":       mv.current_stage,
        "run_id":      mv.run_id,
        "description": mv.description,
    }

3.2 Kubernetes Manifests

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: churn-inference
  namespace: mlops
  labels:
    app: churn-inference
    version: v2
spec:
  replicas: 2
  selector:
    matchLabels:
      app: churn-inference
  template:
    metadata:
      labels:
        app: churn-inference
    spec:
      containers:
        - name: inference
          image: <region>.ocir.io/<namespace>/mlops-infer:<BUILD_TAG>
          ports:
            - containerPort: 8080
          env:
            - name: MLFLOW_TRACKING_URI
              valueFrom:
                secretKeyRef:
                  name: mlops-secrets
                  key: mlflow-uri
            - name: MODEL_NAME
              value: "churn-gbt"
            - name: MODEL_STAGE
              value: "Production"
          resources:
            requests:
              memory: "512Mi"
              cpu:    "250m"
            limits:
              memory: "1Gi"
              cpu:    "1000m"
          readinessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 15
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 20
---
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: churn-inference-svc
  namespace: mlops
spec:
  selector:
    app: churn-inference
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
  type: LoadBalancer

3.3 OKE Deploy Script

#!/bin/bash
# deploy_oke.sh
set -euo pipefail

BUILD_TAG="${1:?Usage: ./deploy_oke.sh <build_tag>}"
NAMESPACE="mlops"
REGION="us-ashburn-1"
OCIR_NAMESPACE="your-tenancy-namespace"

echo "šŸš€ Deploying churn-inference:${BUILD_TAG} to OKE..."

# Login to OCIR
docker login \
  ${REGION}.ocir.io \
  -u "${OCIR_NAMESPACE}/oracleidentitycloudservice/${OCI_USER_EMAIL}" \
  -p "${OCI_AUTH_TOKEN}"

# Substitute build tag in manifests
sed "s/<BUILD_TAG>/${BUILD_TAG}/g" k8s/deployment.yaml | \
  kubectl apply -n ${NAMESPACE} -f -

kubectl apply -n ${NAMESPACE} -f k8s/service.yaml

# Wait for rollout
kubectl rollout status deployment/churn-inference -n ${NAMESPACE} --timeout=5m

echo "✅ Deployment complete."
kubectl get pods -n ${NAMESPACE} -l app=churn-inference

Phase 4 — Monitoring & Governance

4.1 Model Monitoring — Drift Detection

# monitoring/drift_detector.py
"""
Scheduled job: compares production inference distributions
against training baseline. Fires OCI Notification on drift.
"""
import os
import pandas as pd
import numpy as np
from scipy.stats import ks_2samp
import oci
import json

DRIFT_THRESHOLD = 0.05   # KS statistic p-value threshold
BUCKET          = "mlops-artifacts"
NAMESPACE       = os.environ["OCI_TENANCY_NAMESPACE"]

def load_baseline() -> pd.DataFrame:
    return pd.read_parquet(
        f"oci://{BUCKET}@{NAMESPACE}/baselines/churn_train_dist.parquet"
    )

def load_production_window(days: int = 7) -> pd.DataFrame:
    """Load last N days of inference logs from Object Storage."""
    return pd.read_parquet(
        f"oci://{BUCKET}@{NAMESPACE}/inference_logs/recent_{days}d.parquet"
    )

def check_drift(baseline: pd.DataFrame, production: pd.DataFrame) -> dict:
    results = {}
    numeric_cols = baseline.select_dtypes(include=[np.number]).columns

    for col in numeric_cols:
        stat, p_value = ks_2samp(
            baseline[col].dropna(),
            production[col].dropna()
        )
        results[col] = {
            "ks_statistic": round(stat, 4),
            "p_value":      round(p_value, 4),
            "drifted":      p_value < DRIFT_THRESHOLD,
        }
    return results

def send_alert(drifted_features: list):
    config = oci.config.from_file()
    ons    = oci.ons.NotificationDataPlaneClient(config)

    topic_id = os.environ["OCI_NOTIFICATION_TOPIC_ID"]
    message  = json.dumps({
        "alert":    "DATA DRIFT DETECTED",
        "features": drifted_features,
        "action":   "Review and retrain churn-gbt model",
    }, indent=2)

    ons.publish_message(
        topic_id=topic_id,
        message_details=oci.ons.models.MessageDetails(
            title="MLOps Alert: Model Drift",
            body=message,
        )
    )
    print(f"🚨 Alert sent for features: {drifted_features}")

if __name__ == "__main__":
    baseline   = load_baseline()
    production = load_production_window(days=7)
    drift_report = check_drift(baseline, production)

    drifted = [f for f, v in drift_report.items() if v["drifted"]]

    if drifted:
        print(f"⚠️  Drift detected in: {drifted}")
        send_alert(drifted)
    else:
        print("✅ No significant drift detected.")

4.2 Automated Retraining Trigger

# monitoring/retrain_trigger.py
"""
If drift is detected or performance degrades below threshold,
automatically kick off a new OCI DataScience Job (retraining run).
"""
import os
import oci

def trigger_retraining_job(reason: str):
    config  = oci.config.from_file()
    ds      = oci.data_science.DataScienceClient(config)

    job_id  = os.environ["OCI_DATASCIENCE_JOB_ID"]

    run = ds.create_job_run(
        create_job_run_details=oci.data_science.models.CreateJobRunDetails(
            project_id=os.environ["OCI_PROJECT_ID"],
            compartment_id=os.environ["OCI_COMPARTMENT_ID"],
            job_id=job_id,
            display_name=f"auto-retrain-trigger",
            job_environment_configuration_override=None,
            job_configuration_override_details=oci.data_science.models.DefaultJobConfigurationDetails(
                environment_variables={
                    "RETRAIN_REASON": reason,
                    "N_ESTIMATORS":   "300",
                    "MIN_AUC_THRESHOLD": "0.82",
                }
            ),
        )
    )
    print(f"✅ Retraining job launched: {run.data.id}")
    return run.data.id

4.3 OCI IAM Policy — Least Privilege for MLOps

# iam_policies.tf (Terraform)

resource "oci_identity_policy" "mlops_datascience" {
  name           = "mlops-datascience-policy"
  description    = "Allows DataScience resources to read/write Object Storage and publish notifications"
  compartment_id = var.compartment_id

  statements = [
    "Allow dynamic-group mlops-dg to read object-family in compartment mlops-compartment",
    "Allow dynamic-group mlops-dg to manage objects in bucket 'mlops-datasets'",
    "Allow dynamic-group mlops-dg to manage objects in bucket 'mlops-artifacts'",
    "Allow dynamic-group mlops-dg to use ons-topics in compartment mlops-compartment",
    "Allow dynamic-group mlops-dg to manage data-science-jobs in compartment mlops-compartment",
    "Allow dynamic-group mlops-dg to manage data-science-job-runs in compartment mlops-compartment",
    "Allow dynamic-group mlops-dg to read secret-bundles in vault id ${var.vault_id}",
  ]
}

Phase 5 — End-to-End Integration Test

# tests/e2e_test.py
import requests
import json

ENDPOINT = "http://<OKE_LOAD_BALANCER_IP>/predict"

payload = {
    "threshold": 0.5,
    "customers": [
        {
            "tenure":             3,
            "monthly_charges":    85.0,
            "avg_monthly_spend":  21.25,
            "is_long_term":       0,
            "high_spender":       1,
            "contract_one_year":  0,
            "contract_two_year":  0,
        },
        {
            "tenure":             48,
            "monthly_charges":    45.0,
            "avg_monthly_spend":  44.1,
            "is_long_term":       1,
            "high_spender":       0,
            "contract_one_year":  0,
            "contract_two_year":  1,
        },
    ]
}

response = requests.post(ENDPOINT, json=payload)
response.raise_for_status()

results = response.json()
for r in results:
    print(
        f"Customer {r['customer_index']} | "
        f"P(churn)={r['churn_probability']:.2%} | "
        f"Tier={r['risk_tier']} | "
        f"Prediction={'CHURN' if r['churn_prediction'] else 'RETAIN'}"
    )

Expected output:

Customer 0 | P(churn)=78.32% | Tier=HIGH   | Prediction=CHURN
Customer 1 | P(churn)=12.14% | Tier=LOW    | Prediction=RETAIN

Key Lessons — The DBA Perspective

DBA Principle MLOps Translation
Always version your schema Always version your features and training data
Never trust incoming data Run Great Expectations before every training run
Keep backups Store every model artifact to Object Storage
Monitor query performance Monitor model drift and prediction latency
Use least-privilege IAM Scope resource principal policies tightly
Have a rollback plan Keep previous MLFlow model versions in Staging
Automate your maintenance jobs Automate retraining triggers on drift events

Deployment Checklist

  • [ ] OCI Vault secrets configured (MLFlow URI, OCIR auth token)
  • [ ] Dynamic group and IAM policies applied
  • [ ] GitHub repo connected to OCI DevOps project
  • [ ] Build pipeline validated with build_spec.yaml
  • [ ] Training job baseline run completed and logged to MLFlow
  • [ ] Model promoted to Production stage in MLFlow Registry
  • [ ] OKE namespace and secrets created (kubectl create secret)
  • [ ] Kubernetes manifests deployed and readiness probes passing
  • [ ] Drift monitoring job scheduled (OCI Scheduler or cron)
  • [ ] OCI Notification topic wired to ops team email/PagerDuty
  • [ ] End-to-end test passing against live inference endpoint

References

DBA PrincipleMLOps Translation
Schema IntegrityData Quality Gates (Great Expectations)
ACID ComplianceVersioned Models & Data (MLFlow/Object Storage)
Performance TuningHyperparameter Optimization
Point-in-Time RecoveryModel Rollback via MLFlow Stages
Audit LogsOCI Logging & MLFlow Run Tracking

No comments:

Post a Comment

MLOps on OCI: Applying a DBA Mindset to Machine Learning

MLOps is the discipline of treating machine learning systems the way seasoned DBAs treat database pipelines: with version control, monitorin...