MLOps Pipeline Implementation with Kubeflow and Kubernetes: Production-Ready ML Infrastructure
Building robust MLOps pipelines requires a sophisticated orchestration platform that can handle the complexity of machine learning workloads at scale. Kubeflow, running on Kubernetes, provides a comprehensive solution for managing the entire ML lifecycle from data preparation to model deployment and monitoring. This comprehensive guide explores implementing production-ready MLOps pipelines with practical examples and best practices.
MLOps Pipeline Implementation with Kubeflow and Kubernetes
Section 1: Kubeflow Architecture and Core Components
Understanding Kubeflow’s Ecosystem
Kubeflow is a cloud-native platform for machine learning workflows on Kubernetes that provides a unified interface for managing ML pipelines, experiments, and model serving. The platform consists of several key components that work together to provide a complete MLOps solution.
# kubeflow-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: kubeflow
labels:
control-plane: kubeflow
istio-injection: enabled
---
apiVersion: v1
kind: Namespace
metadata:
name: kubeflow-user-example-com
labels:
control-plane: kubeflow
istio-injection: enabled
katib.kubeflow.org/metrics-collector-injection: enabled
Core Kubeflow Components Installation
Installing Kubeflow requires careful orchestration of multiple components. Here’s a production-ready installation approach:
#!/bin/bash
# install-kubeflow.sh
set -euo pipefail
KUBEFLOW_VERSION="v1.8.0"
KUSTOMIZE_VERSION="v5.0.3"
# Install kustomize
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
sudo mv kustomize /usr/local/bin/
# Clone Kubeflow manifests
git clone https://github.com/kubeflow/manifests.git
cd manifests
git checkout ${KUBEFLOW_VERSION}
# Install cert-manager
kustomize build common/cert-manager/cert-manager/base | kubectl apply -f -
kustomize build common/cert-manager/kubeflow-issuer/base | kubectl apply -f -
# Install Istio
kustomize build common/istio-1-17/istio-crds/base | kubectl apply -f -
kustomize build common/istio-1-17/istio-namespace/base | kubectl apply -f -
kustomize build common/istio-1-17/istio-install/base | kubectl apply -f -
# Install OIDC AuthService
kustomize build common/oidc-authservice/base | kubectl apply -f -
# Install Dex
kustomize build common/dex/overlays/istio | kubectl apply -f -
# Install KNative
kustomize build common/knative/knative-serving/overlays/gateways | kubectl apply -f -
kustomize build common/istio-1-17/cluster-local-gateway/base | kubectl apply -f -
# Install Kubeflow Pipelines
kustomize build apps/pipeline/upstream/env/cert-manager/platform-agnostic-multi-user | kubectl apply -f -
# Install KServe
kustomize build contrib/kserve/kserve | kubectl apply -f -
kustomize build contrib/kserve/models-web-app/overlays/kubeflow | kubectl apply -f -
# Install Katib
kustomize build apps/katib/upstream/installs/katib-with-kubeflow | kubectl apply -f -
# Install Central Dashboard
kustomize build apps/centraldashboard/upstream/overlays/kserve | kubectl apply -f -
# Install Admission Webhook
kustomize build apps/admission-webhook/upstream/overlays/cert-manager | kubectl apply -f -
# Install Notebooks & Jupyter Web App
kustomize build apps/jupyter/notebook-controller/upstream/overlays/kubeflow | kubectl apply -f -
kustomize build apps/jupyter/jupyter-web-app/upstream/overlays/istio | kubectl apply -f -
# Install Profiles + KFAM
kustomize build apps/profiles/upstream/overlays/kubeflow | kubectl apply -f -
# Install Volumes Web App
kustomize build apps/volumes-web-app/upstream/overlays/istio | kubectl apply -f -
# Install Tensorboard
kustomize build apps/tensorboard/tensorboards-web-app/upstream/overlays/istio | kubectl apply -f -
kustomize build apps/tensorboard/tensorboard-controller/upstream/overlays/kubeflow | kubectl apply -f -
# Install Training Operator
kustomize build apps/training-operator/upstream/overlays/kubeflow | kubectl apply -f -
# Install User Namespace
kustomize build common/user-namespace/base | kubectl apply -f -
echo "Kubeflow installation completed. Waiting for pods to be ready..."
kubectl wait --for=condition=ready pod --all -n kubeflow --timeout=600s
Advanced Kubeflow Configuration
# kubeflow-custom-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: kubeflow-config
namespace: kubeflow
data:
config.yaml: |
pipeline:
defaultPipelineRoot: "minio://mlpipeline/v2/artifacts"
bucketName: "mlpipeline"
cacheEnabled: true
cacheMaxSizeBytes: "2Gi"
metadata:
grpcPort: "8080"
restPort: "8080"
ui:
clusterDomain: "cluster.local"
mlmdApiServer: "metadata-service.kubeflow:8080"
katib:
suggestionImagePrefix: "docker.io/kubeflowkatib/"
metricsCollectorImagePrefix: "docker.io/kubeflowkatib/"
---
apiVersion: v1
kind: Secret
metadata:
name: mlpipeline-minio-artifact
namespace: kubeflow
type: Opaque
data:
accesskey: bWluaW8= # minio
secretkey: bWluaW8xMjM= # minio123
Section 2: ML Pipeline Development and Automation
Creating Kubeflow Pipelines
Kubeflow Pipelines provide a platform for building and deploying portable, scalable machine learning workflows. Here’s how to create sophisticated pipelines:
# ml_pipeline.py
import kfp
from kfp import dsl
from kfp.components import func_to_container_op, OutputPath, InputPath
import pandas as pd
from typing import NamedTuple
@func_to_container_op
def data_preprocessing(
input_data_path: str,
output_data_path: OutputPath(),
train_test_split_ratio: float = 0.8
) -> NamedTuple('Outputs', [('num_features', int), ('num_samples', int)]):
"""Preprocess data for machine learning pipeline."""
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import joblib
import os
# Load data
df = pd.read_csv(input_data_path)
# Feature engineering
df = df.dropna()
X = df.drop(['target'], axis=1)
y = df['target']
# Split data
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=1-train_test_split_ratio, random_state=42
)
# Scale features
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# Save processed data
os.makedirs(output_data_path, exist_ok=True)
train_df = pd.DataFrame(X_train_scaled, columns=X.columns)
train_df['target'] = y_train.reset_index(drop=True)
train_df.to_csv(f"{output_data_path}/train.csv", index=False)
test_df = pd.DataFrame(X_test_scaled, columns=X.columns)
test_df['target'] = y_test.reset_index(drop=True)
test_df.to_csv(f"{output_data_path}/test.csv", index=False)
# Save scaler
joblib.dump(scaler, f"{output_data_path}/scaler.pkl")
return (len(X.columns), len(df))
@func_to_container_op
def model_training(
data_path: InputPath(),
model_path: OutputPath(),
learning_rate: float = 0.01,
n_estimators: int = 100,
max_depth: int = 10
) -> NamedTuple('Outputs', [('accuracy', float), ('f1_score', float)]):
"""Train machine learning model."""
import pandas as pd
import joblib
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import accuracy_score, f1_score
import os
# Load data
train_df = pd.read_csv(f"{data_path}/train.csv")
test_df = pd.read_csv(f"{data_path}/test.csv")
X_train = train_df.drop(['target'], axis=1)
y_train = train_df['target']
X_test = test_df.drop(['target'], axis=1)
y_test = test_df['target']
# Train model
model = GradientBoostingClassifier(
learning_rate=learning_rate,
n_estimators=n_estimators,
max_depth=max_depth,
random_state=42
)
model.fit(X_train, y_train)
# Evaluate model
y_pred = model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred, average='weighted')
# Save model
os.makedirs(model_path, exist_ok=True)
joblib.dump(model, f"{model_path}/model.pkl")
# Save metrics
metrics = {
'accuracy': accuracy,
'f1_score': f1,
'n_estimators': n_estimators,
'learning_rate': learning_rate,
'max_depth': max_depth
}
import json
with open(f"{model_path}/metrics.json", 'w') as f:
json.dump(metrics, f)
return (accuracy, f1)
@func_to_container_op
def model_validation(
model_path: InputPath(),
validation_threshold: float = 0.85
) -> bool:
"""Validate model performance against threshold."""
import json
import os
# Load metrics
with open(f"{model_path}/metrics.json", 'r') as f:
metrics = json.load(f)
accuracy = metrics['accuracy']
# Validate against threshold
is_valid = accuracy >= validation_threshold
print(f"Model accuracy: {accuracy:.4f}")
print(f"Validation threshold: {validation_threshold}")
print(f"Model validation: {'PASSED' if is_valid else 'FAILED'}")
return is_valid
@dsl.pipeline(
name='ml-training-pipeline',
description='End-to-end ML training pipeline with validation'
)
def ml_training_pipeline(
input_data_url: str = "gs://my-bucket/data/training_data.csv",
train_test_split_ratio: float = 0.8,
learning_rate: float = 0.01,
n_estimators: int = 100,
max_depth: int = 10,
validation_threshold: float = 0.85
):
"""Complete ML training pipeline."""
# Data preprocessing step
preprocess_op = data_preprocessing(
input_data_path=input_data_url,
train_test_split_ratio=train_test_split_ratio
)
# Model training step
training_op = model_training(
data_path=preprocess_op.outputs['output_data_path'],
learning_rate=learning_rate,
n_estimators=n_estimators,
max_depth=max_depth
)
# Model validation step
validation_op = model_validation(
model_path=training_op.outputs['model_path'],
validation_threshold=validation_threshold
)
# Set execution order
training_op.after(preprocess_op)
validation_op.after(training_op)
# Compile pipeline
if __name__ == '__main__':
kfp.compiler.Compiler().compile(ml_training_pipeline, 'ml_training_pipeline.yaml')
Advanced Pipeline Orchestration
# advanced_pipeline.py
import kfp
from kfp import dsl
from kfp.components import func_to_container_op, OutputPath, InputPath
from kubernetes import client as k8s_client
@dsl.pipeline(
name='advanced-ml-pipeline',
description='Advanced ML pipeline with conditional execution and parallel training'
)
def advanced_ml_pipeline(
data_source: str = "postgresql://user:pass@host:5432/db",
model_types: list = ["xgboost", "lightgbm", "catboost"],
enable_feature_selection: bool = True,
cross_validation_folds: int = 5
):
"""Advanced ML pipeline with multiple models and conditional logic."""
# Data ingestion with database connection
data_ingestion_op = dsl.ContainerOp(
name='data-ingestion',
image='gcr.io/my-project/data-ingestion:latest',
arguments=[
'--source', data_source,
'--output-path', '/tmp/data'
],
file_outputs={
'data_path': '/tmp/data/output.csv',
'metadata': '/tmp/data/metadata.json'
}
)
# Conditional feature selection
with dsl.Condition(enable_feature_selection == True):
feature_selection_op = dsl.ContainerOp(
name='feature-selection',
image='gcr.io/my-project/feature-selection:latest',
arguments=[
'--input-data', data_ingestion_op.outputs['data_path'],
'--method', 'recursive_feature_elimination',
'--n_features', '50'
],
file_outputs={
'selected_data': '/tmp/features/selected_data.csv',
'feature_importance': '/tmp/features/importance.json'
}
)
data_for_training = feature_selection_op.outputs['selected_data']
# Use original data if feature selection is disabled
with dsl.Condition(enable_feature_selection == False):
data_for_training = data_ingestion_op.outputs['data_path']
# Parallel model training
model_training_ops = []
for model_type in model_types:
training_op = dsl.ContainerOp(
name=f'train-{model_type}',
image=f'gcr.io/my-project/model-training:latest',
arguments=[
'--input-data', data_for_training,
'--model-type', model_type,
'--cv-folds', str(cross_validation_folds),
'--output-path', f'/tmp/models/{model_type}'
],
file_outputs={
'model': f'/tmp/models/{model_type}/model.pkl',
'metrics': f'/tmp/models/{model_type}/metrics.json'
}
)
model_training_ops.append(training_op)
# Model comparison and selection
model_comparison_op = dsl.ContainerOp(
name='model-comparison',
image='gcr.io/my-project/model-comparison:latest',
arguments=[
'--model-paths'
] + [op.outputs['model'] for op in model_training_ops] + [
'--metrics-paths'
] + [op.outputs['metrics'] for op in model_training_ops],
file_outputs={
'best_model': '/tmp/comparison/best_model.pkl',
'comparison_report': '/tmp/comparison/report.json'
}
)
# Model deployment preparation
deployment_prep_op = dsl.ContainerOp(
name='deployment-preparation',
image='gcr.io/my-project/deployment-prep:latest',
arguments=[
'--model-path', model_comparison_op.outputs['best_model'],
'--deployment-config', '/config/deployment.yaml'
],
file_outputs={
'deployment_artifacts': '/tmp/deployment/artifacts.tar.gz'
}
)
Section 3: Hyperparameter Tuning at Scale with Katib
Katib Configuration for Automated Hyperparameter Optimization
Katib is Kubeflow’s native system for hyperparameter tuning and neural architecture search. Here’s how to implement scalable hyperparameter optimization:
# katib-experiment.yaml
apiVersion: kubeflow.org/v1beta1
kind: Experiment
metadata:
name: ml-hyperparameter-tuning
namespace: kubeflow-user-example-com
spec:
algorithm:
algorithmName: bayesian-optimization
algorithmSettings:
- name: "random_state"
value: "10"
- name: "n_initial_points"
value: "10"
- name: "acq_func"
value: "gp_hedge"
objective:
type: maximize
goal: 0.99
objectiveMetricName: accuracy
additionalMetricNames:
- precision
- recall
- f1_score
parameters:
- name: learning_rate
parameterType: double
feasibleSpace:
min: "0.001"
max: "0.3"
step: "0.001"
- name: n_estimators
parameterType: int
feasibleSpace:
min: "50"
max: "500"
step: "10"
- name: max_depth
parameterType: int
feasibleSpace:
min: "3"
max: "20"
step: "1"
- name: subsample
parameterType: double
feasibleSpace:
min: "0.5"
max: "1.0"
step: "0.1"
- name: min_child_weight
parameterType: int
feasibleSpace:
min: "1"
max: "10"
step: "1"
trialTemplate:
primaryContainerName: training-container
trialParameters:
- name: learning_rate
description: Learning rate for the optimizer
reference: learning_rate
- name: n_estimators
description: Number of boosting rounds
reference: n_estimators
- name: max_depth
description: Maximum tree depth
reference: max_depth
- name: subsample
description: Subsample ratio of training instances
reference: subsample
- name: min_child_weight
description: Minimum sum of instance weight needed in a child
reference: min_child_weight
trialSpec:
apiVersion: batch/v1
kind: Job
spec:
template:
spec:
containers:
- name: training-container
image: gcr.io/my-project/hyperparameter-training:latest
command:
- "python"
- "/app/train.py"
- "--learning_rate=${trialParameters.learning_rate}"
- "--n_estimators=${trialParameters.n_estimators}"
- "--max_depth=${trialParameters.max_depth}"
- "--subsample=${trialParameters.subsample}"
- "--min_child_weight=${trialParameters.min_child_weight}"
- "--output_dir=/tmp/model"
resources:
requests:
memory: "4Gi"
cpu: "2"
nvidia.com/gpu: "1"
limits:
memory: "8Gi"
cpu: "4"
nvidia.com/gpu: "1"
volumeMounts:
- name: data-volume
mountPath: /data
- name: model-volume
mountPath: /tmp/model
volumes:
- name: data-volume
persistentVolumeClaim:
claimName: training-data-pvc
- name: model-volume
persistentVolumeClaim:
claimName: model-storage-pvc
restartPolicy: Never
parallelTrialCount: 4
maxTrialCount: 50
maxFailedTrialCount: 5
metricsCollectorSpec:
source:
fileSystemPath:
path: /tmp/model/metrics.txt
kind: File
collector:
kind: File
earlyStoppingSpec:
algorithmName: medianstop
algorithmSettings:
- name: min_trials_required
value: "5"
- name: start_step
value: "4"
Custom Hyperparameter Training Script
# hyperparameter_training.py
import argparse
import json
import os
import sys
from datetime import datetime
import pandas as pd
import numpy as np
from sklearn.model_selection import cross_val_score
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
import xgboost as xgb
import joblib
import mlflow
import mlflow.xgboost
def parse_args():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(description='Hyperparameter tuning training script')
parser.add_argument('--learning_rate', type=float, required=True,
help='Learning rate for XGBoost')
parser.add_argument('--n_estimators', type=int, required=True,
help='Number of estimators')
parser.add_argument('--max_depth', type=int, required=True,
help='Maximum depth of trees')
parser.add_argument('--subsample', type=float, required=True,
help='Subsample ratio')
parser.add_argument('--min_child_weight', type=int, required=True,
help='Minimum child weight')
parser.add_argument('--data_path', type=str, default='/data/train.csv',
help='Path to training data')
parser.add_argument('--output_dir', type=str, default='/tmp/model',
help='Output directory for model and metrics')
return parser.parse_args()
def load_data(data_path):
"""Load and prepare training data."""
try:
df = pd.read_csv(data_path)
X = df.drop(['target'], axis=1)
y = df['target']
return X, y
except Exception as e:
print(f"Error loading data: {e}")
sys.exit(1)
def train_model(X, y, params):
"""Train XGBoost model with given parameters."""
model = xgb.XGBClassifier(
learning_rate=params['learning_rate'],
n_estimators=params['n_estimators'],
max_depth=params['max_depth'],
subsample=params['subsample'],
min_child_weight=params['min_child_weight'],
random_state=42,
eval_metric='mlogloss',
use_label_encoder=False
)
# Perform cross-validation
cv_scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
# Train final model
model.fit(X, y)
# Make predictions for detailed metrics
y_pred = model.predict(X)
metrics = {
'accuracy': accuracy_score(y, y_pred),
'precision': precision_score(y, y_pred, average='weighted'),
'recall': recall_score(y, y_pred, average='weighted'),
'f1_score': f1_score(y, y_pred, average='weighted'),
'cv_mean': cv_scores.mean(),
'cv_std': cv_scores.std(),
'params': params
}
return model, metrics
def save_results(model, metrics, output_dir):
"""Save model and metrics to output directory."""
os.makedirs(output_dir, exist_ok=True)
# Save model
model_path = os.path.join(output_dir, 'model.pkl')
joblib.dump(model, model_path)
# Save detailed metrics
metrics_path = os.path.join(output_dir, 'metrics.json')
with open(metrics_path, 'w') as f:
json.dump(metrics, f, indent=2)
# Save metrics for Katib (simple format)
katib_metrics_path = os.path.join(output_dir, 'metrics.txt')
with open(katib_metrics_path, 'w') as f:
f.write(f"accuracy={metrics['accuracy']:.6f}\n")
f.write(f"precision={metrics['precision']:.6f}\n")
f.write(f"recall={metrics['recall']:.6f}\n")
f.write(f"f1_score={metrics['f1_score']:.6f}\n")
return model_path, metrics_path
def main():
"""Main training function."""
args = parse_args()
# Prepare parameters
params = {
'learning_rate': args.learning_rate,
'n_estimators': args.n_estimators,
'max_depth': args.max_depth,
'subsample': args.subsample,
'min_child_weight': args.min_child_weight
}
print(f"Starting training with parameters: {params}")
# Load data
X, y = load_data(args.data_path)
print(f"Loaded data: {X.shape[0]} samples, {X.shape[1]} features")
# Start MLflow run
with mlflow.start_run():
# Log parameters
mlflow.log_params(params)
# Train model
model, metrics = train_model(X, y, params)
# Log metrics
mlflow.log_metrics({
'accuracy': metrics['accuracy'],
'precision': metrics['precision'],
'recall': metrics['recall'],
'f1_score': metrics['f1_score'],
'cv_mean': metrics['cv_mean'],
'cv_std': metrics['cv_std']
})
# Save results
model_path, metrics_path = save_results(model, metrics, args.output_dir)
# Log model to MLflow
mlflow.xgboost.log_model(model, "model")
print(f"Training completed successfully!")
print(f"Model saved to: {model_path}")
print(f"Metrics saved to: {metrics_path}")
print(f"Final accuracy: {metrics['accuracy']:.6f}")
if __name__ == "__main__":
main()
Section 4: Model Serving and Versioning
KServe Model Serving Configuration
KServe provides Kubernetes-native model serving with advanced features like auto-scaling, traffic splitting, and A/B testing:
# model-serving-config.yaml
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: ml-model-server
namespace: kubeflow-user-example-com
annotations:
serving.kserve.io/autoscaling.knative.dev: "true"
serving.kserve.io/autoscaling.class: "kpa.autoscaling.knative.dev"
serving.kserve.io/metric: "concurrency"
serving.kserve.io/target: "10"
serving.kserve.io/target-utilization-percentage: "80"
spec:
predictor:
serviceAccountName: kserve-sa
minReplicas: 1
maxReplicas: 10
containers:
- name: kserve-container
image: gcr.io/my-project/model-server:v1.2.0
ports:
- containerPort: 8080
protocol: TCP
resources:
requests:
cpu: 500m
memory: 1Gi
nvidia.com/gpu: "1"
limits:
cpu: "2"
memory: 4Gi
nvidia.com/gpu: "1"
env:
- name: MODEL_PATH
value: "/mnt/models/model.pkl"
- name: PROTOCOL_VERSION
value: "v1"
- name: MODEL_NAME
value: "ml-classifier"
volumeMounts:
- name: model-storage
mountPath: /mnt/models
readOnly: true
livenessProbe:
httpGet:
path: /v1/models/ml-classifier
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /v1/models/ml-classifier
port: 8080
initialDelaySeconds: 15
periodSeconds: 5
volumes:
- name: model-storage
persistentVolumeClaim:
claimName: model-storage-pvc
canaryTrafficPercent: 0
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: kserve-sa
namespace: kubeflow-user-example-com
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kserve-role
rules:
- apiGroups: [""]
resources: ["pods", "services", "endpoints"]
verbs: ["get", "list", "watch"]
- apiGroups: ["serving.kserve.io"]
resources: ["inferenceservices"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kserve-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kserve-role
subjects:
- kind: ServiceAccount
name: kserve-sa
namespace: kubeflow-user-example-com
Custom Model Server Implementation
# model_server.py
import os
import pickle
import json
import logging
import uvicorn
from datetime import datetime
from typing import Dict, List, Any, Optional
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
import pandas as pd
import numpy as np
from prometheus_client import Counter, Histogram, Gauge, generate_latest
from prometheus_client.exposition import CONTENT_TYPE_LATEST
import joblib
import asyncio
import aioredis
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Prometheus metrics
PREDICTION_COUNT = Counter('model_predictions_total', 'Total predictions made')
PREDICTION_LATENCY = Histogram('model_prediction_duration_seconds', 'Prediction latency')
MODEL_LOAD_TIME = Gauge('model_load_time_seconds', 'Time taken to load model')
ACTIVE_CONNECTIONS = Gauge('active_connections', 'Number of active connections')
class PredictionRequest(BaseModel):
instances: List[Dict[str, Any]] = Field(..., description="Input instances for prediction")
parameters: Optional[Dict[str, Any]] = Field(None, description="Additional parameters")
class PredictionResponse(BaseModel):
predictions: List[Dict[str, Any]]
model_name: str
model_version: str
timestamp: str
class ModelServer:
def __init__(self):
self.app = FastAPI(title="ML Model Server", version="1.0.0")
self.model = None
self.scaler = None
self.model_metadata = {}
self.redis_client = None
self.setup_middleware()
self.setup_routes()
def setup_middleware(self):
"""Setup FastAPI middleware."""
self.app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@self.app.middleware("http")
async def add_process_time_header(request: Request, call_next):
ACTIVE_CONNECTIONS.inc()
try:
response = await call_next(request)
return response
finally:
ACTIVE_CONNECTIONS.dec()
async def load_model(self):
"""Load model and preprocessing components."""
start_time = datetime.now()
model_path = os.getenv('MODEL_PATH', '/mnt/models/model.pkl')
scaler_path = os.getenv('SCALER_PATH', '/mnt/models/scaler.pkl')
metadata_path = os.getenv('METADATA_PATH', '/mnt/models/metadata.json')
try:
# Load model
logger.info(f"Loading model from {model_path}")
self.model = joblib.load(model_path)
# Load scaler if exists
if os.path.exists(scaler_path):
logger.info(f"Loading scaler from {scaler_path}")
self.scaler = joblib.load(scaler_path)
# Load metadata if exists
if os.path.exists(metadata_path):
with open(metadata_path, 'r') as f:
self.model_metadata = json.load(f)
# Setup Redis connection for caching
redis_url = os.getenv('REDIS_URL', 'redis://localhost:6379')
try:
self.redis_client = await aioredis.from_url(redis_url)
logger.info("Connected to Redis for caching")
except Exception as e:
logger.warning(f"Could not connect to Redis: {e}")
load_time = (datetime.now() - start_time).total_seconds()
MODEL_LOAD_TIME.set(load_time)
logger.info(f"Model loaded successfully in {load_time:.2f} seconds")
except Exception as e:
logger.error(f"Error loading model: {e}")
raise
def setup_routes(self):
"""Setup FastAPI routes."""
@self.app.on_event("startup")
async def startup_event():
await self.load_model()
@self.app.get("/")
async def root():
return {"message": "ML Model Server", "status": "healthy"}
@self.app.get("/health")
async def health_check():
"""Health check endpoint for Kubernetes probes."""
if self.model is None:
raise HTTPException(status_code=503, detail="Model not loaded")
return {"status": "healthy", "model_loaded": True}
@self.app.get("/v1/models/{model_name}")
async def model_info(model_name: str):
"""Get model information."""
if self.model is None:
raise HTTPException(status_code=503, detail="Model not loaded")
return {
"name": model_name,
"version": self.model_metadata.get("version", "unknown"),
"state": "AVAILABLE",
"status": "ready",
"metadata": self.model_metadata
}
@self.app.post("/v1/models/{model_name}:predict", response_model=PredictionResponse)
async def predict(model_name: str, request: PredictionRequest):
"""Make predictions."""
if self.model is None:
raise HTTPException(status_code=503, detail="Model not loaded")
with PREDICTION_LATENCY.time():
try:
# Check cache first
cache_key = None
if self.redis_client:
cache_key = f"prediction:{hash(str(request.instances))}"
cached_result = await self.redis_client.get(cache_key)
if cached_result:
logger.info("Serving prediction from cache")
PREDICTION_COUNT.inc()
return PredictionResponse.parse_raw(cached_result)
# Prepare input data
df = pd.DataFrame(request.instances)
# Apply preprocessing if scaler is available
if self.scaler:
features_scaled = self.scaler.transform(df)
df_scaled = pd.DataFrame(features_scaled, columns=df.columns)
else:
df_scaled = df
# Make predictions
predictions = self.model.predict(df_scaled)
probabilities = None
# Get prediction probabilities if available
if hasattr(self.model, 'predict_proba'):
probabilities = self.model.predict_proba(df_scaled)
# Format response
prediction_results = []
for i, pred in enumerate(predictions):
result = {"prediction": int(pred) if isinstance(pred, np.integer) else float(pred)}
if probabilities is not None:
result["probabilities"] = probabilities[i].tolist()
prediction_results.append(result)
response = PredictionResponse(
predictions=prediction_results,
model_name=model_name,
model_version=self.model_metadata.get("version", "unknown"),
timestamp=datetime.now().isoformat()
)
# Cache the result
if self.redis_client and cache_key:
await self.redis_client.setex(
cache_key,
300, # 5 minute TTL
response.json()
)
PREDICTION_COUNT.inc()
return response
except Exception as e:
logger.error(f"Prediction error: {e}")
raise HTTPException(status_code=500, detail=f"Prediction failed: {str(e)}")
@self.app.get("/metrics")
async def metrics():
"""Prometheus metrics endpoint."""
return generate_latest().decode('utf-8')
def create_app():
"""Create and configure the FastAPI application."""
server = ModelServer()
return server.app
if __name__ == "__main__":
app = create_app()
uvicorn.run(
app,
host="0.0.0.0",
port=int(os.getenv("PORT", "8080")),
workers=1,
loop="asyncio"
)
Section 5: Production Monitoring and Observability
MLflow Integration for Model Tracking
# mlflow_tracking.py
import mlflow
import mlflow.sklearn
import mlflow.xgboost
from mlflow.tracking import MlflowClient
import pandas as pd
import numpy as np
from datetime import datetime
import logging
import os
from typing import Dict, Any, List
class MLflowModelTracker:
"""Comprehensive MLflow tracking for ML models."""
def __init__(self, tracking_uri: str = None, experiment_name: str = "default"):
self.tracking_uri = tracking_uri or os.getenv('MLFLOW_TRACKING_URI', 'http://mlflow:5000')
self.experiment_name = experiment_name
self.client = MlflowClient(tracking_uri=self.tracking_uri)
self.setup_experiment()
def setup_experiment(self):
"""Setup MLflow experiment."""
try:
self.experiment = self.client.get_experiment_by_name(self.experiment_name)
if self.experiment is None:
experiment_id = self.client.create_experiment(
name=self.experiment_name,
tags={
"created_by": "kubeflow-pipeline",
"created_at": datetime.now().isoformat()
}
)
self.experiment = self.client.get_experiment(experiment_id)
except Exception as e:
logging.error(f"Error setting up MLflow experiment: {e}")
raise
def log_model_training(self,
model,
metrics: Dict[str, float],
params: Dict[str, Any],
dataset_info: Dict[str, Any],
artifacts: Dict[str, str] = None):
"""Log complete model training information."""
with mlflow.start_run(experiment_id=self.experiment.experiment_id) as run:
# Log parameters
mlflow.log_params(params)
# Log metrics
mlflow.log_metrics(metrics)
# Log dataset information
mlflow.log_params({
f"dataset_{k}": v for k, v in dataset_info.items()
})
# Log model based on type
if hasattr(model, 'get_booster'): # XGBoost
mlflow.xgboost.log_model(model, "model")
else: # Sklearn
mlflow.sklearn.log_model(model, "model")
# Log additional artifacts
if artifacts:
for name, path in artifacts.items():
mlflow.log_artifact(path, name)
# Log system information
mlflow.log_params({
"python_version": os.sys.version,
"training_time": datetime.now().isoformat(),
"node_name": os.getenv('NODE_NAME', 'unknown'),
"pod_name": os.getenv('POD_NAME', 'unknown')
})
return run.info.run_id
def register_model(self,
run_id: str,
model_name: str,
stage: str = "Staging",
description: str = None):
"""Register model in MLflow Model Registry."""
model_uri = f"runs:/{run_id}/model"
try:
# Register model
registered_model = mlflow.register_model(
model_uri=model_uri,
name=model_name,
tags={
"registered_at": datetime.now().isoformat(),
"source_run": run_id
}
)
# Transition to specified stage
self.client.transition_model_version_stage(
name=model_name,
version=registered_model.version,
stage=stage,
archive_existing_versions=False
)
# Add description if provided
if description:
self.client.update_model_version(
name=model_name,
version=registered_model.version,
description=description
)
return registered_model
except Exception as e:
logging.error(f"Error registering model: {e}")
raise
def promote_model(self, model_name: str, version: str, target_stage: str):
"""Promote model to production stage."""
try:
# Archive current production models
if target_stage.lower() == "production":
current_prod_models = self.client.get_latest_versions(
model_name, stages=["Production"]
)
for model in current_prod_models:
self.client.transition_model_version_stage(
name=model_name,
version=model.version,
stage="Archived"
)
# Promote new model
self.client.transition_model_version_stage(
name=model_name,
version=version,
stage=target_stage,
archive_existing_versions=False
)
logging.info(f"Model {model_name} v{version} promoted to {target_stage}")
except Exception as e:
logging.error(f"Error promoting model: {e}")
raise
def compare_models(self, model_name: str, metric_name: str = "accuracy"):
"""Compare model versions by metric."""
try:
# Get all versions
versions = self.client.search_model_versions(f"name='{model_name}'")
comparison_data = []
for version in versions:
run = self.client.get_run(version.run_id)
metric_value = run.data.metrics.get(metric_name)
comparison_data.append({
"version": version.version,
"stage": version.current_stage,
"run_id": version.run_id,
metric_name: metric_value,
"created_at": version.creation_timestamp
})
return pd.DataFrame(comparison_data).sort_values(
metric_name, ascending=False
)
except Exception as e:
logging.error(f"Error comparing models: {e}")
raise
Comprehensive Monitoring Dashboard
# monitoring-stack.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-config
namespace: kubeflow
data:
prometheus.yml: |
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "ml_alerts.yml"
scrape_configs:
- job_name: 'kubeflow-pipelines'
kubernetes_sd_configs:
- role: pod
namespaces:
names:
- kubeflow
- kubeflow-user-example-com
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- job_name: 'ml-model-servers'
kubernetes_sd_configs:
- role: service
namespaces:
names:
- kubeflow-user-example-com
relabel_configs:
- source_labels: [__meta_kubernetes_service_label_app]
action: keep
regex: ml-model-server
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
action: keep
regex: true
- job_name: 'katib-experiments'
kubernetes_sd_configs:
- role: pod
namespaces:
names:
- kubeflow-user-example-com
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_katib_kubeflow_org_trial]
action: keep
regex: (.+)
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
ml_alerts.yml: |
groups:
- name: ml_model_alerts
rules:
- alert: ModelPredictionLatencyHigh
expr: histogram_quantile(0.95, model_prediction_duration_seconds_bucket) > 5
for: 5m
labels:
severity: warning
annotations:
summary: "High prediction latency detected"
description: "95th percentile prediction latency is {{ $value }}s"
- alert: ModelAccuracyDegraded
expr: model_accuracy < 0.85
for: 10m
labels:
severity: critical
annotations:
summary: "Model accuracy has degraded"
description: "Model accuracy is {{ $value }}, below threshold"
- alert: PipelineJobFailed
expr: increase(pipeline_job_failures_total[5m]) > 0
for: 1m
labels:
severity: critical
annotations:
summary: "ML Pipeline job failed"
description: "{{ $value }} pipeline jobs have failed in the last 5 minutes"
- alert: HyperparameterTuningStalled
expr: time() - katib_experiment_last_update_time > 3600
for: 5m
labels:
severity: warning
annotations:
summary: "Hyperparameter tuning experiment stalled"
description: "Katib experiment has not updated in over 1 hour"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: prometheus
namespace: kubeflow
spec:
replicas: 1
selector:
matchLabels:
app: prometheus
template:
metadata:
labels:
app: prometheus
spec:
containers:
- name: prometheus
image: prom/prometheus:v2.45.0
ports:
- containerPort: 9090
volumeMounts:
- name: config-volume
mountPath: /etc/prometheus
- name: storage-volume
mountPath: /prometheus
args:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
- '--storage.tsdb.retention.time=15d'
- '--web.enable-lifecycle'
- '--web.enable-admin-api'
volumes:
- name: config-volume
configMap:
name: prometheus-config
- name: storage-volume
persistentVolumeClaim:
claimName: prometheus-storage
---
apiVersion: v1
kind: Service
metadata:
name: prometheus
namespace: kubeflow
spec:
selector:
app: prometheus
ports:
- port: 9090
targetPort: 9090
type: ClusterIP
Section 6: Cost Optimization Strategies
Resource Management and Auto-scaling
# cost-optimization-policies.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: ml-workload-quota
namespace: kubeflow-user-example-com
spec:
hard:
requests.cpu: "50"
requests.memory: 200Gi
requests.nvidia.com/gpu: "10"
limits.cpu: "100"
limits.memory: 400Gi
limits.nvidia.com/gpu: "10"
persistentvolumeclaims: "20"
requests.storage: 1Ti
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: ml-model-server-pdb
namespace: kubeflow-user-example-com
spec:
minAvailable: 1
selector:
matchLabels:
app: ml-model-server
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: ml-model-server-hpa
namespace: kubeflow-user-example-com
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: ml-model-server
minReplicas: 1
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
- type: Pods
pods:
metric:
name: concurrent_requests
target:
type: AverageValue
averageValue: "10"
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 100
periodSeconds: 30
- type: Pods
value: 2
periodSeconds: 30
selectPolicy: Max
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: ml-training-scaler
namespace: kubeflow-user-example-com
spec:
scaleTargetRef:
name: ml-training-deployment
minReplicaCount: 0
maxReplicaCount: 10
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus:9090
metricName: training_queue_length
threshold: '5'
query: training_queue_length
- type: cron
metadata:
timezone: America/New_York
start: "0 9 * * 1-5" # Scale up weekdays at 9 AM
end: "0 18 * * 1-5" # Scale down weekdays at 6 PM
desiredReplicas: "3"
Cost Monitoring and Optimization
# cost_optimization.py
import kubernetes
from kubernetes import client, config
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import logging
from typing import Dict, List, Tuple
import prometheus_client
from prometheus_client.parser import text_string_to_metric_families
import requests
class KubeflorwCostOptimizer:
"""Kubeflow cost optimization and monitoring."""
def __init__(self, prometheus_url: str = "http://prometheus:9090"):
self.prometheus_url = prometheus_url
config.load_incluster_config()
self.v1 = client.CoreV1Api()
self.apps_v1 = client.AppsV1Api()
self.metrics_v1beta1 = client.CustomObjectsApi()
def get_resource_usage(self, namespace: str = "kubeflow-user-example-com") -> pd.DataFrame:
"""Get current resource usage for all pods."""
try:
pods = self.v1.list_namespaced_pod(namespace)
usage_data = []
for pod in pods.items:
if pod.status.phase == "Running":
# Get pod metrics
try:
metrics = self.metrics_v1beta1.get_namespaced_custom_object(
group="metrics.k8s.io",
version="v1beta1",
namespace=namespace,
plural="pods",
name=pod.metadata.name
)
cpu_usage = 0
memory_usage = 0
for container in metrics.get('containers', []):
cpu_usage += self._parse_cpu(container['usage'].get('cpu', '0'))
memory_usage += self._parse_memory(container['usage'].get('memory', '0'))
# Get resource requests/limits
cpu_request = 0
memory_request = 0
cpu_limit = 0
memory_limit = 0
for container in pod.spec.containers:
if container.resources:
if container.resources.requests:
cpu_request += self._parse_cpu(
container.resources.requests.get('cpu', '0')
)
memory_request += self._parse_memory(
container.resources.requests.get('memory', '0')
)
if container.resources.limits:
cpu_limit += self._parse_cpu(
container.resources.limits.get('cpu', '0')
)
memory_limit += self._parse_memory(
container.resources.limits.get('memory', '0')
)
usage_data.append({
'pod_name': pod.metadata.name,
'namespace': namespace,
'cpu_usage_cores': cpu_usage,
'memory_usage_bytes': memory_usage,
'cpu_request_cores': cpu_request,
'memory_request_bytes': memory_request,
'cpu_limit_cores': cpu_limit,
'memory_limit_bytes': memory_limit,
'cpu_utilization': (cpu_usage / cpu_request * 100) if cpu_request > 0 else 0,
'memory_utilization': (memory_usage / memory_request * 100) if memory_request > 0 else 0,
'created_at': pod.metadata.creation_timestamp
})
except Exception as e:
logging.warning(f"Could not get metrics for pod {pod.metadata.name}: {e}")
return pd.DataFrame(usage_data)
except Exception as e:
logging.error(f"Error getting resource usage: {e}")
return pd.DataFrame()
def analyze_cost_efficiency(self, usage_df: pd.DataFrame) -> Dict:
"""Analyze cost efficiency and provide recommendations."""
if usage_df.empty:
return {"status": "error", "message": "No usage data available"}
# Calculate waste metrics
cpu_waste = usage_df['cpu_request_cores'] - usage_df['cpu_usage_cores']
memory_waste = usage_df['memory_request_bytes'] - usage_df['memory_usage_bytes']
# Identify optimization opportunities
over_provisioned_cpu = usage_df[usage_df['cpu_utilization'] < 30]
over_provisioned_memory = usage_df[usage_df['memory_utilization'] < 30]
under_provisioned_cpu = usage_df[usage_df['cpu_utilization'] > 90]
under_provisioned_memory = usage_df[usage_df['memory_utilization'] > 90]
recommendations = []
# CPU optimization recommendations
if len(over_provisioned_cpu) > 0:
avg_cpu_waste = over_provisioned_cpu['cpu_usage_cores'].sum()
recommendations.append({
"type": "cpu_over_provisioning",
"affected_pods": len(over_provisioned_cpu),
"potential_savings_cores": avg_cpu_waste,
"description": f"Reduce CPU requests for {len(over_provisioned_cpu)} pods with <30% utilization"
})
if len(under_provisioned_cpu) > 0:
recommendations.append({
"type": "cpu_under_provisioning",
"affected_pods": len(under_provisioned_cpu),
"description": f"Increase CPU requests for {len(under_provisioned_cpu)} pods with >90% utilization"
})
# Memory optimization recommendations
if len(over_provisioned_memory) > 0:
avg_memory_waste = over_provisioned_memory['memory_usage_bytes'].sum()
recommendations.append({
"type": "memory_over_provisioning",
"affected_pods": len(over_provisioned_memory),
"potential_savings_gb": avg_memory_waste / (1024**3),
"description": f"Reduce memory requests for {len(over_provisioned_memory)} pods with <30% utilization"
})
if len(under_provisioned_memory) > 0:
recommendations.append({
"type": "memory_under_provisioning",
"affected_pods": len(under_provisioned_memory),
"description": f"Increase memory requests for {len(under_provisioned_memory)} pods with >90% utilization"
})
# Idle pod detection
idle_pods = usage_df[
(usage_df['cpu_utilization'] < 5) &
(usage_df['memory_utilization'] < 10)
]
if len(idle_pods) > 0:
recommendations.append({
"type": "idle_pods",
"affected_pods": len(idle_pods),
"pod_names": idle_pods['pod_name'].tolist(),
"description": f"Consider terminating {len(idle_pods)} idle pods"
})
return {
"timestamp": datetime.now().isoformat(),
"total_pods_analyzed": len(usage_df),
"average_cpu_utilization": usage_df['cpu_utilization'].mean(),
"average_memory_utilization": usage_df['memory_utilization'].mean(),
"total_cpu_waste_cores": cpu_waste.sum(),
"total_memory_waste_gb": memory_waste.sum() / (1024**3),
"recommendations": recommendations
}
def _parse_cpu(self, cpu_str: str) -> float:
"""Parse CPU string to cores."""
if not cpu_str or cpu_str == '0':
return 0.0
if cpu_str.endswith('m'):
return float(cpu_str[:-1]) / 1000
elif cpu_str.endswith('n'):
return float(cpu_str[:-1]) / 1000000000
else:
return float(cpu_str)
def _parse_memory(self, memory_str: str) -> int:
"""Parse memory string to bytes."""
if not memory_str or memory_str == '0':
return 0
units = {
'Ki': 1024,
'Mi': 1024**2,
'Gi': 1024**3,
'Ti': 1024**4,
'K': 1000,
'M': 1000**2,
'G': 1000**3,
'T': 1000**4
}
for unit, multiplier in units.items():
if memory_str.endswith(unit):
return int(float(memory_str[:-len(unit)]) * multiplier)
return int(memory_str)
def generate_cost_report(self, namespace: str = "kubeflow-user-example-com") -> Dict:
"""Generate comprehensive cost optimization report."""
usage_df = self.get_resource_usage(namespace)
analysis = self.analyze_cost_efficiency(usage_df)
# Get historical trends
historical_data = self._get_historical_metrics()
report = {
"report_date": datetime.now().isoformat(),
"namespace": namespace,
"current_analysis": analysis,
"historical_trends": historical_data,
"action_items": self._generate_action_items(analysis)
}
return report
def _get_historical_metrics(self) -> Dict:
"""Get historical resource usage metrics from Prometheus."""
queries = {
"avg_cpu_utilization": 'avg(rate(container_cpu_usage_seconds_total[1h]))',
"avg_memory_utilization": 'avg(container_memory_usage_bytes / container_spec_memory_limit_bytes)',
"cost_trend": 'sum(kube_pod_container_resource_requests_cpu_cores * 0.048)' # Assuming $0.048 per CPU hour
}
historical_data = {}
for metric_name, query in queries.items():
try:
url = f"{self.prometheus_url}/api/v1/query_range"
params = {
'query': query,
'start': (datetime.now() - timedelta(days=7)).timestamp(),
'end': datetime.now().timestamp(),
'step': '1h'
}
response = requests.get(url, params=params)
data = response.json()
if data['status'] == 'success':
historical_data[metric_name] = data['data']['result']
except Exception as e:
logging.warning(f"Could not fetch historical data for {metric_name}: {e}")
return historical_data
def _generate_action_items(self, analysis: Dict) -> List[Dict]:
"""Generate specific action items based on analysis."""
action_items = []
for recommendation in analysis.get('recommendations', []):
if recommendation['type'] == 'cpu_over_provisioning':
action_items.append({
"priority": "medium",
"action": "Reduce CPU requests",
"command": "kubectl patch deployment <deployment-name> -p '{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"<container-name>\",\"resources\":{\"requests\":{\"cpu\":\"<new-value>\"}}}]}}}}'",
"estimated_savings": f"{recommendation.get('potential_savings_cores', 0):.2f} CPU cores"
})
elif recommendation['type'] == 'memory_over_provisioning':
action_items.append({
"priority": "medium",
"action": "Reduce memory requests",
"command": "kubectl patch deployment <deployment-name> -p '{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"<container-name>\",\"resources\":{\"requests\":{\"memory\":\"<new-value>\"}}}]}}}}'",
"estimated_savings": f"{recommendation.get('potential_savings_gb', 0):.2f} GB memory"
})
elif recommendation['type'] == 'idle_pods':
for pod_name in recommendation.get('pod_names', []):
action_items.append({
"priority": "high",
"action": f"Investigate idle pod: {pod_name}",
"command": f"kubectl delete pod {pod_name}",
"estimated_savings": "Variable based on pod resources"
})
return action_items
# Usage example
if __name__ == "__main__":
optimizer = KubeflorwCostOptimizer()
report = optimizer.generate_cost_report()
print(json.dumps(report, indent=2))
This comprehensive guide provides a production-ready approach to implementing MLOps pipelines with Kubeflow on Kubernetes. The examples include sophisticated pipeline orchestration, automated hyperparameter tuning, robust model serving, comprehensive monitoring, and cost optimization strategies that are essential for running ML workloads at scale in production environments.
The implementation covers all aspects of the ML lifecycle from data preprocessing and model training to deployment and monitoring, providing a solid foundation for building enterprise-grade MLOps infrastructure.