MLOps Fundamentals: as 9 etapas chave para passar de zero a operacional

MLOps Fundamentals: o essencial em um artigo — código real, diagramas e etapas concretas, extraídos de um curso de 72 lições.

MLOps Fundamentals: as 9 etapas chave para passar de zero a operacional

Todo mundo pode aprender MLOps Fundamentals — desde que siga as etapas na ordem correta. Condensamos um curso completo de 72 lições em um percurso claro, com os trechos de código mais úteis.

tl;dr
  • Instalar o ambiente MLOps
  • Descobrir o MLOps
  • Versionamento de dados e modelos
  • Pipelines ML com MLflow
  • Containerizar os modelos ML
~$ cat ./parcours.md # MLOps Fundamentals — 12 capítulos
01
Instalar o ambiente MLOps
→ Instalar Python, conda e as ferramentas essenciais→ Configurar Git, GitHub e as boas práticas+ 1 mais lições
02
Descobrir o MLOps
→ O que é MLOps e por que é indispensável?→ O ciclo de vida de um modelo ML em produção+ 1 mais lições
03
Versionamento de dados e modelos
→ Por que versionar os dados e os modelos?→ DVC — instalação e primeiros passos+ 1 mais lições
04
Pipelines ML com MLflow
→ MLflow Tracking : registrar as experiências ML→ MLflow Model Registry : gerenciar o ciclo de vida dos modelos+ 1 mais lições
05
Containerizar os modelos ML
→ Por que Docker é essencial para o MLOps→ Dockerfile para um modelo ML : build e run+ 1 mais lições
06
Implantar com FastAPI
→ FastAPI para servir modelos ML→ Construir uma API de predição completa+ 1 mais lições
07
CI-CD para o ML
→ O que é CI/CD para o ML?→ GitHub Actions : automatizar os testes ML+ 2 mais lições
08
Monitoramento dos modelos em produção
→ Por que os modelos se degradam em produção?→ Detectar o Data Drift com Evidently+ 1 mais lições
🏁
Projeto final (+ 4 capítulos no caminho)
→ Você sai com um projeto concreto e demonstrável

Guia Completo do Projeto Final – Passo a Passo

Curso MLOps Fundamentals • Detecção de Fraude por Cartão de Crédito • Pipeline MLOps End-to-End

NOTE📚 Sobre este guia
Este guia detalhado acompanha você passo a passo na realização do projeto final. Cada seção corresponde a uma etapa-chave do pipeline MLOps. Siga as etapas em ordem. Todo o código é fornecido e explicado. Duração estimada: 8–12 horas no total.

① Etapa 1 – Inicialização do projeto e ambiente

Crie a estrutura do projeto e o ambiente 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

Crie o arquivo 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

Crie a estrutura de pastas:

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"

② Etapa 2 – Geração e versionamento dos dados

Crie src/generate_synthetic_data.py (caso não tenha acesso ao dataset Kaggle):

output
"""Gera um dataset sintético de fraude de cartão 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 gerado: {len(df)} linhas")
    print(f"Fraudes: {df['Class'].sum()} ({df['Class'].mean()*100:.3f}%)")

Dockerfile para um modelo ML: build e run

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

NOTE🎯 Objetivos de aprendizagem
  • Compreender cada instrução de um Dockerfile para um modelo ML
  • Escrever um Dockerfile completo para uma API scikit-learn com FastAPI
  • Construir uma imagem Docker com docker build
  • Executar e testar um contêiner ML com docker run
  • Depurar os problemas comuns de contêineres ML

1. Estrutura do projeto ML a ser containerizado

Antes de escrever o Dockerfile, vejamos a estrutura do projeto que vamos containerizar. Trata-se de uma API de classificação de vinhos baseada em um modelo Random Forest treinado com scikit-learn, servido via FastAPI.

output
wine-classifier/
→── Dockerfile              # Nosso arquivo de configuração Docker
→── .dockerignore           # Arquivos a excluir da imagem
→── requirements.txt        # Dependências Python
→── src/
├──   app.py               # Aplicação FastAPI (ponto de entrada)
├──   predict.py           # Lógica de previsão
├──   preprocessing.py     # Pré-processamento das features
└── models/
    └── rf_classifier.pkl   # Modelo Random Forest serializado
    └── scaler.pkl           # StandardScaler salvo

O arquivo requirements.txt

O requirements.txt lista todas as dependências Python com suas versões fixadas. Isso é crucial para a reprodutibilidade: sem versão fixada, pip install poderia baixar uma versão mais recente e incompatível.

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

# Serialização do modelo
joblib==1.3.2

# Validação de dados
pydantic==2.6.3
WARNING⚠ Sempre fixar as versões!
Evite scikit-learn>=1.0 ou scikit-learn sem versão. Se o scikit-learn publicar a versão 2.0 com mudanças de API, seu contêiner quebrará no próximo rebuild. Use sempre scikit-learn==1.4.1.

A aplicação 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 classificação da qualidade do vinho baseada em Random Forest",
    version="1.0.0"
)

# Carregamento do modelo na inicialização (não a cada requisição)
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": "bom vinho" if prediction >= 6 else "vinho médio"
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

2. O Dockerfile completo e comentado

Aqui está o Dockerfile completo. Vamos analisar cada instrução em detalhe imediatamente após.

output
# ============================================================
# Dockerfile para um modelo scikit-learn servido com FastAPI
# ============================================================

# Etapa 1: Imagem base
FROM python:3.11-slim

# Etapa 2: Metadados da imagem
LABEL maintainer="mlops-team@exemple.com"
LABEL version="1.0.0"
LABEL description="Wine Quality Classifier - Random Forest API"

# Etapa 3: Variáveis de ambiente do sistema
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

# Etapa 4: Diretório de trabalho no contêiner
WORKDIR /app

# Etapa 5: Dependências do sistema (se necessário)
RUN apt-get update && apt-get install -y --no-install-recommends \
    libgomp1 \
    && rm -rf /var/lib/apt/lists/*

# Etapa 6: Copiar E instalar as dependências Python (camada em cache)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Etapa 7: Copiar o código-fonte
COPY src/ ./src/

# Etapa 8: Copiar o modelo ML
COPY models/ ./models/

# Etapa 9: Expor a porta da API
EXPOSE 8000

# Etapa 10: Usuário não-root para segurança
RUN useradd --create-home --shell /bin/bash appuser
USER appuser

# Etapa 11: Comando de inicialização
CMD ["uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "8000"]

3. Análise detalhada de cada instrução

3.1 FROM python:3.11-slim

FROM define a imagem base. python:3.11-slim é a versão leve da imagem Python oficial:

Para scikit-learn, slim é o bom compromisso: suficientemente pequeno, mas compatível com as extensões C do numpy/scipy.

WARNING⚠ Evitar :latest
FROM python:latest pode passar de Python 3.11 para 3.12 da noite para o dia e quebrar seu código. Sempre especifique a versão exata: python:3.11-slim ou até python:3.11.8-slim.
TIP💡 Para GPU
Se seu modelo usar GPU (TensorFlow, PyTorch), use:
FROM nvidia/cuda:12.1-cudnn8-runtime-ubuntu22.04

3.2 Variáveis de ambiente ENV

output
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1
Variável Efeito Por que ativar?
PYTHONDONTWRITEBYTECODE=1 Sem arquivos .pyc Reduz o tamanho da imagem, evita arquivos desnecessários
PYTHONUNBUFFERED=1 Logs em tempo real Indispensável para ver os logs em docker logs imediatamente
PIP_NO_CACHE_DIR=1 Sem cache pip na imagem Reduz o tamanho final da imagem (pode economizar 100-200 MB)
PIP_DISABLE_PIP_VERSION_CHECK=1 Sem verificação de atualização do pip Acelera o build, evita avisos desnecessários

3.3 WORKDIR /app

WORKDIR define o diretório de trabalho no contêiner. Todas as instruções seguintes (COPY, RUN, CMD) são executadas a partir desse diretório. Se o diretório não existir, o Docker o cria automaticamente.

TIP💡 Boa prática: Sempre use /app como diretório de trabalho para aplicações Python. Evite trabalhar diretamente em / ou /root.

Otimização de imagens Docker ML

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

NOTE🎯 Objetivos de aprendizagem
  • Compreender por que o tamanho das imagens Docker ML é um desafio crítico em produção
  • Dominar os multi-stage builds para separar construção e execução
  • Configurar um .dockerignore eficaz para projetos ML
  • Explorar o cache de camadas Docker ao máximo
  • Aplicar as melhores práticas para imagens ML de produção

1. Por que o tamanho das imagens ML é crítico

Ao contrário de uma aplicação web clássica (algumas dezenas de MB), uma imagem Docker ML pode facilmente atingir 2 a 8 GB. Esse tamanho tem consequências diretas no seu fluxo de trabalho MLOps:

🚬 Implantação lenta

Baixar uma imagem de 5 GB em um cluster Kubernetes leva de 10 a 20 minutos. Em caso de auto-scaling sob carga, isso é inaceitável.

💸 Custo de armazenamento

Armazenar 50 versões de uma imagem de 4 GB no AWS ECR = 200 GB × 0,10 US$/GB/mês = 20 US$/mês apenas para armazenamento.

🔐 Superfície de ataque

Cada ferramenta adicional (compiladores, utilitários de sistema) é uma potencial vulnerabilidade de segurança. Uma imagem mínima é mais segura.

2. Comparação: imagens pesadas vs imagens slim

Abordagem Imagem base Tamanho típico Tempo de pull (100 Mbit/s) Vulnerabilidades
🔴 Imagem pesada (ingênua) python:3.11 + todas as ferramentas ~2,1 GB ~170 s Muito numerosas
🟡 Imagem slim (boa prática) 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 Poucas
🆕 Distroless gcr.io/distroless/python3 ~180 MB ~14 s Muito poucas
🟢 Alpine (com precauções) python:3.11-alpine ~100 MB ~8 s Mínimas
WARNING⚠ Alpine e as bibliotecas ML
Alpine usa musl libc em vez de glibc. Numpy, scikit-learn, pandas e PyTorch são compilados para glibc. O uso de Alpine força a compilação a partir das fontes, o que leva 30 a 60 minutos e pode falhar. Reserve Alpine para microsserviços sem dependências C. Para ML, prefira python:3.11-slim.

3. O .dockerignore: sua primeira linha de defesa

Antes mesmo do multi-stage build, o .dockerignore é a otimização mais simples e imediata. Ele impede que o Docker envie arquivos desnecessários (ou até perigosos) para o daemon de build.

TIP💡 O contexto de build Docker
Quando você executa docker build ., o Docker envia todo o diretório atual para o daemon Docker (chamado de «contexto de build»). Sem .dockerignore, isso inclui seu dataset de 10 GB, os notebooks Jupyter, os ambientes virtuais Python, os arquivos de configuração secretos…
output
# .dockerignore para um projeto ML

# Controle de versão
.git/
.gitignore
.github/

# Ambientes Python virtuais (crítico! pode ocupar centenas de MB)
.venv/
venv/
env/
ENV/
__pycache__/
*.py[cod]
*.pyo
.pytest_cache/
.mypy_cache/

# Dados ML (nunca incluir na imagem!)
data/
datasets/
*.csv
*.parquet
*.arrow
*.feather

# Notebooks Jupyter (não necessários em produção)
*.ipynb
.ipynb_checkpoints/

# Arquivos de configuração local
.env
.env.local
.env.development
*.env

# Documentação e testes (inúteis em produção)
docs/
tests/
README.md
CHANGELOG.md

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

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

# Logs e arquivos temporários
logs/
*.log
tmp/
temp/

# Arquivos MLflow e experimentos
mlruns/
mlflow.db

# Modelos de teste (manter apenas o modelo de produção)
models/experiments/
models/checkpoints/

Medir o impacto do .dockerignore

bash
# Verificar o tamanho do contexto de build ANTES do .dockerignore
docker build -t test-before . 2>&1 | head -5
# Sending build context to Docker daemon  4.521GB  <-- sem .dockerignore!

# Verificar o tamanho do contexto de build DEPOIS do .dockerignore
docker build -t test-after . 2>&1 | head -5
# Sending build context to Docker daemon  12.34MB  <-- com .dockerignore

4. Os multi-stage builds para ML

Um multi-stage build usa várias instruções FROM em um único Dockerfile. Cada etapa produz uma camada intermediária, e apenas a última etapa é mantida na imagem final. Isso permite:

4.1 Multi-stage build para um modelo scikit-learn

output
# ============================================================
# Multi-stage Dockerfile para Wine Classifier (produção)
# ============================================================

# ---- ETAPA 1: Builder ----
# Esta etapa instala tudo, inclusive as ferramentas de compilação
FROM python:3.11-slim AS builder

# Variáveis de ambiente para o build
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1

WORKDIR /build

# Instalar as ferramentas de build do sistema (NÃO estarão na imagem 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 em um diretório local isolado (--prefix)
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt


# ---- ETAPA 2: Imagem de produção (final) ----
# Imagem final ultraleve: não contém gcc, build-essential, etc.
FROM python:3.11-slim AS production

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

# Copiar apenas as bibliotecas instaladas do builder
COPY --from=builder /install /usr/local

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

# Copiar o código-fonte e o modelo
COPY src/ ./src/
COPY models/ ./models/

# Segurança: usuário não-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💡 Ganho de tamanho típico com multi-stage
Ao eliminar gcc, g++, build-essential e os arquivos intermediários de compilação da imagem final, você geralmente ganha entre 200 e 600 MB dependendo das dependências C/C++ utilizadas.

4.2 Multi-stage build com etapa de teste

output
# ============================================================
# Multi-stage Dockerfile com etapa de teste 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 teste ----
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 produção ----
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-plus-loin

Este artigo cobre os trechos mais úteis — o curso completo MLOps Fundamentals (13 capítulos, 72 lições, exercícios corrigidos e projeto final) leva você até o fim.

./acessar-o-curso-completo curso gratuito: Dominar Claude Code

FAQ

Quanto tempo para aprender MLOps Fundamentals?
Com uma progressão estruturada (13 capítulos, 72 lições curtas e práticas), você atinge um nível operacional em algumas semanas dedicando 30 a 60 minutos por dia. O importante é praticar cada conceito imediatamente.
Precisa de pré-requisitos?
É melhor estar confortável com os fundamentos da área: este conteúdo vai em profundidade, com casos reais.
Por onde começar concretamente?
Reproduza os comandos deste artigo, depois siga o curso completo MLOps Fundamentals: ele encadeia as 72 lições em ordem, com exercícios e projeto final.

📬 Quer receber este tipo de guia toda semana? Inscreva-se gratuitamente — código real, zero enrolação.