Pular para o conteúdo principal

3 publicações com a etiqueta "aula"

Ver todas as etiquetas

Aula prática: containerizar uma API Python em produção

· 3 min para ler
Lab IA/SW
Laboratórios de IA & Desenvolvimento de Software

Aula prática: containerizar uma API Python em produção

Containerizar uma API Python parece simples — até você precisar de logs estruturados, healthcheck, graceful shutdown e multi-stage build para uma imagem final < 100MB. Esta aula reproduz o template que usamos no lab.

O Dockerfile final (que vamos construir)

# ===== Stage 1: build =====
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# ===== Stage 2: runtime =====
FROM python:3.12-slim
WORKDIR /app

# Non-root user
RUN useradd -m -u 1000 -s /bin/bash app

# Copy installed deps from builder
COPY --from=builder /root/.local /home/app/.local
ENV PATH=/home/app/.local/bin:$PATH

# Copy app code
COPY --chown=app:app . .
USER app

EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1

CMD ["gunicorn", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", \
"--bind", "0.0.0.0:8000", "--access-logfile", "-", \
"main:app"]

Resultado: imagem final de ~92MB.

Passo 1 — Multi-stage cuts your image in half

O truque do multi-stage é instalar dependências em um stage descartável (builder), e copiar apenas o resultado para o stage final. Sem isso, sua imagem carrega gcc, headers do Python, cache do pip — tudo desnecessário em runtime.

ApproachTamanho final
python:3.12 + pip install980 MB
python:3.12-slim + pip install280 MB
python:3.12-slim + multi-stage92 MB
python:3.12-alpine + multi-stage68 MB ⚠️

Alpine é menor, mas dá problemas com libs que dependem de glibc (numpy, scipy, pandas). Para APIs simples, vale. Para data-science, fique no slim.

Passo 2 — Por que gunicorn + uvicorn workers?

FastAPI é ASGI (async). Uvicorn é o servidor ASGI de referência. Mas em produção:

  • Uvicorn sozinho: processo único. Se ele crashar, sua API morre.
  • Gunicorn como process manager: fork de N workers, restart automático, graceful shutdown.
  • Workers do tipo UvicornWorker: gunicorn gerencia, uvicorn executa o async.

A combinação dá robustez de process manager + performance async. É o padrão recomendado pela própria documentação do FastAPI.

Passo 3 — Healthcheck que faz sentido

@app.get("/health")
async def health():
# 1. Processo está respondendo? Sim (a função executou)
# 2. Conexão com banco está ok?
try:
await db.execute("SELECT 1")
except Exception:
return Response(status_code=503)
return {"status": "ok"}

Esse /health é usado tanto pelo HEALTHCHECK do Dockerfile quanto pelo liveness/readiness do Kubernetes. Não retorne sempre 200: o healthcheck precisa falhar se o banco cair, senão o orchestrator nunca remove o pod doente do load balancer.

Passo 4 — Logs estruturados

import logging, json

class JSONFormatter(logging.Formatter):
def format(self, record):
return json.dumps({
"ts": self.formatTime(record),
"level": record.levelname,
"msg": record.getMessage(),
"module": record.module,
})

handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logging.basicConfig(level=logging.INFO, handlers=[handler])

Logs JSON são indispensáveis para qualquer ferramenta de observability (CloudWatch, Datadog, Grafana Loki) extrair campos.

Passo 5 — Graceful shutdown

from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app):
# startup
yield
# shutdown: close connections, flush logs
await db.close()

app = FastAPI(lifespan=lifespan)

Sem isso, ao escalar para baixo, conexões DB ficam abertas no servidor, requests em vôo são abortadas.

Resultado

Imagem final pronta para:

  • ECS Fargate
  • Cloud Run
  • Kubernetes
  • Docker Compose em produção (com Caddy/Nginx na frente)

E roda em qualquer plataforma sem mudança.

Kubernetes vs Serverless em 2026: o framework de decisão que usamos no lab

· 3 min para ler
Lab IA/SW
Laboratórios de IA & Desenvolvimento de Software

Kubernetes vs Serverless em 2026: o framework de decisão que usamos no lab

A pergunta volta a cada novo serviço que vamos provisionar: K8s ou serverless? Depois de 3 anos rodando os dois em paralelo no laboratório, sistematizamos a decisão num framework de 5 dimensões.

A regra rápida (pra quem só quer o palpite)

Se você tem menos de 4 microserviços ou não tem time dedicado de plataforma, comece em serverless. Migre para K8s só quando a conta da serverless ficar maior que o custo de manter um cluster.

Esse é o ponto onde a maioria dos times decide errado — começa em K8s "porque é o futuro", passa 6 meses configurando ingress, secrets, monitoring, helm, e nunca chega a entregar o produto.

As 5 dimensões

1. Custo em escala

Lambda Cloud Run K8s (EKS)
1 req/s $0.20/mês $0.50/mês $73/mês (cluster)
100 req/s $20/mês $35/mês $73/mês
10k req/s $2000/mês $1200/mês $400/mês (3 nodes)
100k req/s $20k/mês $8k/mês $1200/mês

O ponto de inflexão fica entre 1k e 5k req/s dependendo do payload. Abaixo disso, serverless ganha. Acima, K8s ganha.

2. Cold start tolerância

WorkloadTolera cold start?
Webhook receiver✅ sim, raro
API pública pra usuário⚠️ depende (300ms tolerável, 3s não)
Processamento batch✅ totalmente
Inferência de IA em tempo real❌ K8s/SageMaker
Conexão WebSocket persistente❌ K8s

3. Estado e conexões persistentes

Serverless não mantém estado entre invocações. Se você precisa:

  • Conexão pool de DB persistente
  • Cache em memória
  • WebSocket de longa duração
  • Subscriptions de eventos com state

K8s é o caminho. Lambda + ElastiCache + DynamoDB resolve, mas com mais glue code.

4. Equipe disponível

Setup mínimo para K8s saudável:
- 1 SRE/Platform engineer dedicado
- CI/CD configurado (ArgoCD ou Flux)
- Observability stack (Prometheus + Grafana + Loki)
- Política de RBAC + secrets management
- Backup/disaster recovery do etcd

Setup mínimo para Lambda saudável:
- AWS SAM ou Serverless Framework
- CloudWatch alarms básicos

Tempo de setup inicial: 2-4 semanas K8s vs 1 dia Lambda.

5. Lock-in com provedor

Serverless = lock-in alto. AWS Lambda + API Gateway + DynamoDB é difícil migrar pra GCP.

K8s = portável (em teoria). Na prática, manifests rodam em qualquer cluster, mas operadores específicos (AWS Load Balancer Controller, EFS CSI driver, etc.) prendem ao cloud.

Casos reais do laboratório

Pipeline de embedding (jobs assíncronos, batch grande)

Escolha: Lambda + SQS. Cold start de 800ms é irrelevante quando o job inteiro leva 2 minutos. Custo cai pra ~$3/mês fora dos picos.

API de inferência em tempo real (latência crítica, p99 < 100ms)

Escolha: EKS + nodes com GPU. Modelos carregados em memória, sem cold start.

Webhooks de integrações (10-50 req/s, latência não-crítica)

Escolha: Cloud Run. Auto-scaling até zero quando ninguém está chamando, paga só pelo que usa.

Dashboard interno (10 users simultâneos, baixíssima escala)

Escolha: Cloud Run (free tier cobre tudo). K8s seria caro pra esse volume.

Conclusão

Não existe escolha "futura-prova". Existe escolha certa para o estágio atual do produto e tamanho do time. Comece simples, migre quando a conta ou a complexidade da serverless exceder o custo de operar um cluster.

Aula: observability em Go do zero — traces, metrics e logs em 1 hora

· 3 min para ler
Lab IA/SW
Laboratórios de IA & Desenvolvimento de Software

Aula: observability em Go do zero — traces, metrics e logs em 1 hora

Esta aula é um passo-a-passo prático: pegamos uma API Go básica e instrumentamos os três pilares de observability (logs, metrics, traces) em 60 minutos. No final, você tem um serviço pronto para produção com visibilidade completa.

O serviço base (5 minutos)

package main

import (
"net/http"
"github.com/go-chi/chi/v5"
)

func main() {
r := chi.NewRouter()
r.Get("/users/{id}", getUser)
http.ListenAndServe(":8080", r)
}

func getUser(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
// ... lógica de fetch user
w.Write([]byte(`{"id":"` + id + `","name":"Maria"}`))
}

Funcional, mas cego. Vamos instrumentar.

Pilar 1: logs estruturados com slog (10 min)

A stdlib Go ganhou log/slog em 1.21 — não precisa mais de Zap, Logrus, etc.

import (
"log/slog"
"os"
)

func init() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
}

func getUser(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
slog.Info("user fetch",
"user_id", id,
"method", r.Method,
"path", r.URL.Path,
)
// ...
}

Output:

{"time":"2026-05-02T10:00:00Z","level":"INFO","msg":"user fetch","user_id":"42","method":"GET","path":"/users/42"}

Já consumível por CloudWatch, Loki, Datadog sem parser custom.

Pilar 2: metrics com Prometheus (15 min)

import "github.com/prometheus/client_golang/prometheus/promhttp"
import "github.com/prometheus/client_golang/prometheus"

var (
requestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total de requests HTTP",
},
[]string{"method", "endpoint", "status"},
)
requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Buckets: []float64{0.01, 0.05, 0.1, 0.3, 1, 3, 10},
},
[]string{"endpoint"},
)
)

func init() {
prometheus.MustRegister(requestsTotal, requestDuration)
}

// Middleware
func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
ww := &statusWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(ww, r)
requestsTotal.WithLabelValues(r.Method, r.URL.Path, fmt.Sprintf("%d", ww.status)).Inc()
requestDuration.WithLabelValues(r.URL.Path).Observe(time.Since(start).Seconds())
})
}

// main()
r.Use(metricsMiddleware)
r.Handle("/metrics", promhttp.Handler())

Agora curl :8080/metrics retorna formato Prometheus pronto pra scrape.

Pilar 3: traces com OpenTelemetry (25 min)

import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("localhost:4317"),
otlptracegrpc.WithInsecure(),
)
if err != nil { return nil, err }

tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("users-api"),
)),
)
otel.SetTracerProvider(tp)
return tp, nil
}

func main() {
ctx := context.Background()
tp, _ := initTracer(ctx)
defer tp.Shutdown(ctx)

r := chi.NewRouter()
r.Use(metricsMiddleware)
r.Method("GET", "/users/{id}",
otelhttp.NewHandler(http.HandlerFunc(getUser), "users.get"),
)
http.ListenAndServe(":8080", r)
}

func getUser(w http.ResponseWriter, r *http.Request) {
ctx, span := otel.Tracer("users").Start(r.Context(), "fetch_from_db")
defer span.End()
// ... fetch user, e cada query passa ctx pra trazer no trace
}

Spans aparecem em qualquer backend OTel: Jaeger, Tempo, Datadog APM, Honeycomb.

Stack docker-compose pra testar localmente

services:
prometheus:
image: prom/prometheus
ports: ["9090:9090"]
volumes: ["./prometheus.yml:/etc/prometheus/prometheus.yml"]

grafana:
image: grafana/grafana
ports: ["3000:3000"]

tempo:
image: grafana/tempo
command: ["-config.file=/etc/tempo.yaml"]
ports: ["4317:4317"]

Em 1 hora você tem:

  • Dashboard de RPS, latência, erro por endpoint
  • Distributed tracing entre serviços
  • Logs estruturados queryable

Custo operacional

  • Self-hosted (lab): 1 VM 4GB roda Prometheus + Grafana + Loki + Tempo sem suar.
  • Managed: Grafana Cloud free tier (10k metrics series, 50GB logs, 50GB traces) cobre serviços médios.
  • Datadog: caro mas turn-key. Considere para times sem SRE.

Conclusão

Observability não é luxo. É o que te avisa antes do usuário reclamar. Em Go, com a stack acima, o custo de instrumentar é ~200 linhas pra um serviço médio — e o ganho é noites dormindo durante incidentes.