MLOps Fundamentals: las 9 etapas clave para pasar de cero a operativo

MLOps Fundamentals : lo esencial en un artículo — código real, diagramas y pasos concretos, extractos de un curso de 72 lecciones.

MLOps Fundamentals: las 9 etapas clave para pasar de cero a operativo

Todo el mundo puede aprender MLOps Fundamentals — siempre que siga los pasos en el orden correcto. Hemos condensado un curso completo de 72 lecciones en un recorrido claro, con los extractos de código más útiles.

tl;dr
  • Instalar el entorno MLOps
  • Descubrir MLOps
  • Versionado de datos y modelos
  • Pipelines ML con MLflow
  • Containerizar los modelos ML
~$ cat ./parcours.md # MLOps Fundamentals — 12 capítulos
01
Instalar el entorno MLOps
→ Instalar Python, conda y las herramientas esenciales→ Configurar Git, GitHub y las buenas prácticas+ 1 más lecciones
02
Descubrir el MLOps
→ ¿Qué es el MLOps y por qué es indispensable?→ El ciclo de vida de un modelo ML en producción+ 1 más lecciones
03
Versionado de datos y modelos
→ ¿Por qué versionar los datos y los modelos?→ DVC — instalación y primeros pasos+ 1 más lecciones
04
Pipelines ML con MLflow
→ MLflow Tracking : registrar los experimentos ML→ MLflow Model Registry : gestionar el ciclo de vida de los modelos+ 1 más lecciones
05
Containerizar los modelos ML
→ ¿Por qué Docker es esencial para el MLOps→ Dockerfile para un modelo ML : build y run+ 1 más lecciones
06
Desplegar con FastAPI
→ FastAPI para servir modelos ML→ Construir una API de predicción completa+ 1 más lecciones
07
CI-CD para el ML
→ ¿Qué es el CI/CD para el ML?→ GitHub Actions : automatizar las pruebas ML+ 2 más lecciones
08
Monitoreo de modelos en producción
→ ¿Por qué los modelos se degradan en producción?→ Detectar el Data Drift con Evidently+ 1 más lecciones
🏁
Proyecto final (+ 4 capítulos en el camino)
→ Te vas con un proyecto concreto y demostrable

Guía completa del proyecto final – Paso a paso

Curso MLOps Fundamentals • Detección de fraude con tarjeta de crédito • Pipeline MLOps End-to-End

NOTE📚 Acerca de esta guía
Esta guía detallada te acompaña paso a paso en la realización del proyecto final. Cada sección corresponde a una etapa clave del pipeline MLOps. Sigue los pasos en orden. Todo el código está proporcionado y explicado. Duración estimada: 8–12 horas en total.

① Paso 1 – Inicialización del proyecto y entorno

Crea la estructura del proyecto y el entorno Conda:

bash
mkdir fraud-detection-mlops
cd fraud-detection-mlops
git init
git config user.email "vous@email.com"
git config user.name "Votre Nom"

conda create -n fraud-mlops python=3.10 -y
conda activate fraud-mlops

pip install scikit-learn xgboost pandas numpy mlflow dvc \
            fastapi uvicorn pydantic evidently \
            pytest pytest-cov httpx joblib matplotlib \
            seaborn imbalanced-learn flake8 black

conda env export > environment.yml

Crea el archivo requirements.txt:

output
scikit-learn==1.4.0
xgboost==2.0.3
pandas==2.1.4
numpy==1.26.3
mlflow==2.10.0
dvc==3.38.1
fastapi==0.109.0
uvicorn==0.27.0
pydantic==2.5.3
evidently==0.4.16
pytest==7.4.4
pytest-cov==4.1.0
httpx==0.26.0
joblib==1.3.2
matplotlib==3.8.2
seaborn==0.13.1
imbalanced-learn==0.11.0

Crea la estructura de carpetas:

bash
mkdir -p data/raw data/processed src api monitoring/reports tests models .github/workflows
output
__pycache__/
*.pyc
*.pyo
.env
models/
mlruns/
*.pkl
data/raw/
data/processed/
monitoring/reports/*.html
monitoring/reports/*.json
.coverage
htmlcov/
bash
dvc init
git add .
git commit -m "feat: init project structure and DVC"

② Paso 2 – Generación y versionado de los datos

Crea src/generate_synthetic_data.py (si no tienes acceso al dataset de Kaggle):

output
"""Genera un dataset sintético de fraude con tarjeta de crédito."""
import pandas as pd
import numpy as np

def generate_fraud_dataset(n_samples=50000, fraud_ratio=0.002, random_state=42):
    np.random.seed(random_state)
    n_fraud = int(n_samples * fraud_ratio)
    n_legit = n_samples - n_fraud

    legit = pd.DataFrame({
        f'V{i}': np.random.normal(0, 1, n_legit) for i in range(1, 29)
    })
    legit['Amount'] = np.abs(np.random.exponential(scale=88, size=n_legit))
    legit['Time'] = np.sort(np.random.uniform(0, 172800, n_legit))
    legit['Class'] = 0

    fraud = pd.DataFrame({
        f'V{i}': np.random.normal(np.random.uniform(-3, 3), 2, n_fraud) for i in range(1, 29)
    })
    fraud['Amount'] = np.abs(np.random.exponential(scale=122, size=n_fraud))
    fraud['Time'] = np.random.uniform(0, 172800, n_fraud)
    fraud['Class'] = 1

    df = pd.concat([legit, fraud], ignore_index=True)
    df = df.sample(frac=1, random_state=random_state).reset_index(drop=True)
    return df

if __name__ == "__main__":
    df = generate_fraud_dataset()
    df.to_csv("data/raw/creditcard.csv", index=False)
    print(f"Dataset generado: {len(df)} filas")
    print(f"Fraudes: {df['Class'].sum()} ({df['Class'].mean()*100:.3f}%)")

Dockerfile para un modelo ML: build y run

Curso MLOps Fundamentals — Capítulo 04 — Containerizar los modelos ML

NOTE🎯 Objetivos de aprendizaje
  • Comprender cada instrucción de un Dockerfile para un modelo ML
  • Escribir un Dockerfile completo para una API scikit-learn con FastAPI
  • Construir una imagen Docker con docker build
  • Ejecutar y probar un contenedor ML con docker run
  • Depurar los problemas comunes de contenedores ML

1. Estructura del proyecto ML a containerizar

Antes de escribir el Dockerfile, veamos la estructura del proyecto que vamos a containerizar. Se trata de una API de clasificación de vinos basada en un modelo Random Forest entrenado con scikit-learn, servido a través de FastAPI.

output
wine-classifier/
â└â─â─ Dockerfile              # Nuestro archivo de configuración Docker
â└â─â─ .dockerignore           # Archivos a excluir de la imagen
â└â─â─ requirements.txt        # Dependencias Python
â└â─â─ src/
â├â─â─   app.py               # Aplicación FastAPI (punto de entrada)
â├â─â─   predict.py           # Lógica de predicción
â├â─â─   preprocessing.py     # Preprocesamiento de features
â└â─â─ models/
    â└â─â─ rf_classifier.pkl   # Modelo Random Forest serializado
    â└â─â─ scaler.pkl           # StandardScaler guardado

El archivo requirements.txt

El requirements.txt lista todas las dependencias Python con sus versiones fijadas. Es crucial para la reproducibilidad: sin versión fijada, pip install podría descargar una versión más reciente e incompatible.

output
# requirements.txt
# Framework web
fastapi==0.110.0
uvicorn[standard]==0.27.1

# Machine Learning
scikit-learn==1.4.1
numpy==1.26.4
pandas==2.2.1

# Serialización del modelo
joblib==1.3.2

# Validación de datos
pydantic==2.6.3
WARNING⚠ ¡Fijar siempre las versiones!
Evita scikit-learn>=1.0 o scikit-learn sin versión. Si scikit-learn publica la versión 2.0 con cambios de API, tu contenedor fallará en el próximo rebuild. Usa siempre scikit-learn==1.4.1.

La aplicación FastAPI (src/app.py)

output
# src/app.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import joblib
import numpy as np
import os

app = FastAPI(
    title="Wine Quality Classifier API",
    description="API de clasificación de la calidad del vino basada en Random Forest",
    version="1.0.0"
)

# Carga del modelo al inicio (no en cada petición)
MODEL_PATH = os.getenv("MODEL_PATH", "/app/models/rf_classifier.pkl")
SCALER_PATH = os.getenv("SCALER_PATH", "/app/models/scaler.pkl")

model = joblib.load(MODEL_PATH)
scaler = joblib.load(SCALER_PATH)

class WineFeatures(BaseModel):
    fixed_acidity: float
    volatile_acidity: float
    citric_acid: float
    residual_sugar: float
    chlorides: float
    free_sulfur_dioxide: float
    total_sulfur_dioxide: float
    density: float
    pH: float
    sulphates: float
    alcohol: float

@app.get("/health")
def health_check():
    return {"status": "ok", "model": "rf_classifier", "version": "1.0.0"}

@app.post("/predict")
def predict(features: WineFeatures):
    try:
        X = np.array([[
            features.fixed_acidity, features.volatile_acidity,
            features.citric_acid, features.residual_sugar,
            features.chlorides, features.free_sulfur_dioxide,
            features.total_sulfur_dioxide, features.density,
            features.pH, features.sulphates, features.alcohol
        ]])
        X_scaled = scaler.transform(X)
        prediction = model.predict(X_scaled)[0]
        probability = model.predict_proba(X_scaled)[0].max()
        return {
            "quality": int(prediction),
            "confidence": round(float(probability), 4),
            "label": "buen vino" if prediction >= 6 else "vino medio"
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

2. El Dockerfile completo y comentado

Aquí está el Dockerfile completo. Analizaremos cada instrucción en detalle inmediatamente después.

output
# ============================================================
# Dockerfile para un modelo scikit-learn servido con FastAPI
# ============================================================

# Paso 1: Imagen base
FROM python:3.11-slim

# Paso 2: Metadatos de la imagen
LABEL maintainer="mlops-team@exemple.com"
LABEL version="1.0.0"
LABEL description="Wine Quality Classifier - Random Forest API"

# Paso 3: Variables de entorno del sistema
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

# Paso 4: Directorio de trabajo en el contenedor
WORKDIR /app

# Paso 5: Dependencias del sistema (si es necesario)
RUN apt-get update && apt-get install -y --no-install-recommends \
    libgomp1 \
    && rm -rf /var/lib/apt/lists/*

# Paso 6: Copiar E instalar las dependencias Python (capa en caché)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Paso 7: Copiar el código fuente
COPY src/ ./src/

# Paso 8: Copiar el modelo ML
COPY models/ ./models/

# Paso 9: Exponer el puerto de la API
EXPOSE 8000

# Paso 10: Usuario no root para seguridad
RUN useradd --create-home --shell /bin/bash appuser
USER appuser

# Paso 11: Comando de inicio
CMD ["uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "8000"]

3. Análisis detallado de cada instrucción

3.1 FROM python:3.11-slim

FROM define la imagen base. python:3.11-slim es la versión ligera de la imagen oficial de Python:

Para scikit-learn, slim es el buen compromiso: suficientemente pequeño, pero compatible con las extensiones C de numpy/scipy.

WARNING⚠ Evitar :latest
FROM python:latest puede pasar de Python 3.11 a 3.12 de un día para otro y romper tu código. Especifica siempre la versión exacta: python:3.11-slim o incluso python:3.11.8-slim.
TIP💡 Para GPU
Si tu modelo utiliza la GPU (TensorFlow, PyTorch), usa:
FROM nvidia/cuda:12.1-cudnn8-runtime-ubuntu22.04

3.2 Variables de entorno ENV

output
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1
Variable Efecto ¿Por qué activarla?
PYTHONDONTWRITEBYTECODE=1 Sin archivos .pyc Reduce el tamaño de la imagen, evita archivos innecesarios
PYTHONUNBUFFERED=1 Logs en tiempo real Indispensable para ver los logs en docker logs inmediatamente
PIP_NO_CACHE_DIR=1 Sin caché de pip en la imagen Reduce el tamaño final de la imagen (puede ahorrar 100-200 MB)
PIP_DISABLE_PIP_VERSION_CHECK=1 Sin verificación de actualización de pip Acelera el build, evita advertencias innecesarias

3.3 WORKDIR /app

WORKDIR define el directorio de trabajo en el contenedor. Todas las instrucciones siguientes (COPY, RUN, CMD) se ejecutan desde este directorio. Si el directorio no existe, Docker lo crea automáticamente.

TIP💡 Buena práctica: Usa siempre /app como directorio de trabajo para las aplicaciones Python. Evita trabajar directamente en / o /root.

Optimización de imágenes Docker ML

Curso MLOps Fundamentals — Capítulo 04 — Containerizar los modelos ML

NOTE🎯 Objetivos de aprendizaje
  • Comprender por qué el tamaño de las imágenes Docker ML es un desafío crítico en producción
  • Dominar los multi-stage builds para separar construcción y ejecución
  • Configurar un .dockerignore eficaz para proyectos ML
  • Aprovechar al máximo la caché de capas de Docker
  • Aplicar las mejores prácticas para imágenes ML de producción

1. Por qué el tamaño de las imágenes ML es crítico

A diferencia de una aplicación web clásica (unas decenas de MB), una imagen Docker ML puede alcanzar fácilmente 2 a 8 GB. Este tamaño tiene consecuencias directas en tu flujo de trabajo MLOps:

🚬 Despliegue lento

Descargar una imagen de 5 GB en un clúster de Kubernetes tarda entre 10 y 20 minutos. En caso de autoescalado bajo carga, es inaceptable.

💸 Costo de almacenamiento

Almacenar 50 versiones de una imagen de 4 GB en AWS ECR = 200 GB × 0,10 $/GB/mes = 20 $/mes solo por el almacenamiento.

🔐 Superficie de ataque

Cada herramienta adicional (compiladores, utilidades del sistema) es una vulnerabilidad de seguridad potencial. Una imagen mínima es más segura.

2. Comparación: imágenes pesadas vs imágenes slim

Enfoque Imagen base Tamaño típico Tiempo de pull (100 Mbit/s) Vulnerabilidades
🔴 Imagen pesada (ingenua) python:3.11 + todas las herramientas ~2,1 GB ~170 s Muy numerosas
🟡 Imagen slim (buena práctica) python:3.11-slim ~700 MB ~56 s Moderadas
🟢 Multi-stage slim Build: python:3.11-slim
Run: python:3.11-slim
~350 MB ~28 s Pocas
🆕 Distroless gcr.io/distroless/python3 ~180 MB ~14 s Muy pocas
🟢 Alpine (con precauciones) python:3.11-alpine ~100 MB ~8 s Mínimas
WARNING⚠ Alpine y las bibliotecas ML
Alpine utiliza musl libc en lugar de glibc. Numpy, scikit-learn, pandas y PyTorch están compilados para glibc. El uso de Alpine obliga a compilar desde las fuentes, lo que tarda 30 a 60 minutos y puede fallar. Reserva Alpine para microservicios sin dependencias C. Para ML, prefiere python:3.11-slim.

3. El .dockerignore: tu primera línea de defensa

Incluso antes del multi-stage build, el .dockerignore es la optimización más simple e inmediata. Impide que Docker envíe archivos innecesarios (o incluso peligrosos) al daemon de build.

TIP💡 El contexto de build de Docker
Cuando ejecutas docker build ., Docker envía todo el directorio actual al daemon de Docker (llamado «contexto de build»). Sin .dockerignore, esto incluye tu dataset de 10 GB, los notebooks Jupyter, los entornos virtuales de Python, los archivos de configuración secretos…
output
# .dockerignore para un proyecto ML

# Control de versiones
.git/
.gitignore
.github/

# Entornos virtuales de Python (¡crítico! puede ocupar cientos de MB)
.venv/
venv/
env/
ENV/
__pycache__/
*.py[cod]
*.pyo
.pytest_cache/
.mypy_cache/

# Datos ML (¡nunca incluir en la imagen!)
data/
datasets/
*.csv
*.parquet
*.arrow
*.feather

# Notebooks Jupyter (no necesarios en producción)
*.ipynb
.ipynb_checkpoints/

# Archivos de configuración local
.env
.env.local
.env.development
*.env

# Documentación y tests (inútiles en producción)
docs/
tests/
README.md
CHANGELOG.md

# Artefactos de build
dist/
build/
*.egg-info/
htmlcov/
.coverage

# IDE y editores
.vscode/
.idea/
*.swp
*.swo
.DS_Store
Thumbs.db

# Logs y archivos temporales
logs/
*.log
tmp/
temp/

# Archivos MLflow y experimentos
mlruns/
mlflow.db

# Modelos de prueba (mantener solo el modelo de producción)
models/experiments/
models/checkpoints/

Medir el impacto del .dockerignore

bash
# Verificar el tamaño del contexto de build ANTES de .dockerignore
docker build -t test-before . 2>&1 | head -5
# Sending build context to Docker daemon  4.521GB  <-- ¡sin .dockerignore!

# Verificar el tamaño del contexto de build DESPUÉS de .dockerignore
docker build -t test-after . 2>&1 | head -5
# Sending build context to Docker daemon  12.34MB  <-- con .dockerignore

4. Los multi-stage builds para ML

Un multi-stage build utiliza varias instrucciones FROM en un solo Dockerfile. Cada etapa produce una capa intermedia, y solo la última etapa se conserva en la imagen final. Esto permite:

4.1 Multi-stage build para un modelo scikit-learn

output
# ============================================================
# Multi-stage Dockerfile para Wine Classifier (producción)
# ============================================================

# ---- ETAPA 1: Builder ----
# Esta etapa instala todo, incluidas las herramientas de compilación
FROM python:3.11-slim AS builder

# Variables de entorno para el build
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1

WORKDIR /build

# Instalar las herramientas de build del sistema (NO estarán en la imagen final)
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    g++ \
    build-essential \
    libgomp1 \
    && rm -rf /var/lib/apt/lists/*

# Copiar e instalar en un directorio local aislado (--prefix)
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt


# ---- ETAPA 2: Imagen de producción (final) ----
# Imagen final ultraligera: NO contiene gcc, build-essential, etc.
FROM python:3.11-slim AS production

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

# Copiar solo las bibliotecas instaladas desde el builder
COPY --from=builder /install /usr/local

# Dependencias del sistema runtime (mínimas)
RUN apt-get update && apt-get install -y --no-install-recommends \
    libgomp1 \
    && rm -rf /var/lib/apt/lists/*

# Copiar el código fuente y el modelo
COPY src/ ./src/
COPY models/ ./models/

# Seguridad: usuario no root
RUN useradd --create-home --shell /bin/bash appuser && \
    chown -R appuser:appuser /app
USER appuser

EXPOSE 8000

CMD ["uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
TIP💡 Ganancia de tamaño típica con multi-stage
Al eliminar gcc, g++, build-essential y los archivos intermedios de compilación de la imagen final, generalmente ganas entre 200 y 600 MB según las dependencias C/C++ utilizadas.

4.2 Multi-stage build con etapa de test

output
# ============================================================
# Multi-stage Dockerfile con etapa de test integrada
# ============================================================

FROM python:3.11-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt


# ---- Etapa de test ----
FROM base AS test
COPY requirements-dev.txt .
RUN pip install --no-cache-dir -r requirements-dev.txt
COPY src/ ./src/
COPY models/ ./models/
COPY tests/ ./tests/
RUN pytest tests/ -v --tb=short


# ---- Etapa de producción ----
FROM base AS production
COPY src/ ./src/
COPY models/ ./models/
RUN useradd --create-home appuser && chown -R appuser /app
USER appuser
EXPOSE 8000
CMD ["uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "8000"]
va-más-lejos

Este artículo cubre los extractos más útiles — el curso completo MLOps Fundamentals (13 capítulos, 72 lecciones, ejercicios corregidos y proyecto final) te lleva hasta el final.

./acceder-al-curso-completo curso gratuito: Dominar Claude Code

FAQ

¿Cuánto tiempo se necesita para aprender MLOps Fundamentals?
Con una progresión estructurada (13 capítulos, 72 lecciones cortas y prácticas), se alcanza un nivel operativo en unas semanas dedicando 30 a 60 minutos al día. Lo importante es practicar cada concepto de inmediato.
¿Se necesitan requisitos previos?
Es mejor estar cómodo con los fundamentos del dominio: este contenido profundiza, con casos reales.
¿Por dónde empezar concretamente?
Reproduce los comandos de este artículo y luego sigue el curso completo MLOps Fundamentals: encadena las 72 lecciones en orden, con ejercicios y proyecto final.

📬 ¿Quieres recibir este tipo de guía cada semana? Suscríbete gratis — código real, cero palabrería.