Containerización: Docker y Kubernetes para IA
¿Por Qué Containers para IA?
Las aplicaciones de IA tienen dependencias complejas: versiones específicas de Python, CUDA, bibliotecas de ML y modelos pesados. Docker garantiza que el entorno de desarrollo sea idéntico al de producción.
Dockerfile para Aplicación RAG
# ── Multi-stage build para aplicación RAG ─────────────
# Stage 1: Builder
FROM python:3.12-slim AS builder
WORKDIR /build
# Instalar dependencias del sistema para compilación
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc g++ && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# Stage 2: Runtime
FROM python:3.12-slim AS runtime
WORKDIR /app
# Solo copiar las dependencias instaladas
COPY --from=builder /install /usr/local
# Copiar el código fuente
COPY src/ ./src/
COPY prompts/ ./prompts/
COPY config/ ./config/
# Usuario no-root
RUN useradd --create-home appuser
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
EXPOSE 8000
# Uvicorn con workers configurables
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", \
"--port", "8000", "--workers", "4"]
Docker Compose para Desarrollo Local
# docker-compose.yml
services:
# ── API RAG ─────────────────────────────────────────
api:
build: .
ports:
- "8000:8000"
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- QDRANT_HOST=qdrant
- REDIS_HOST=redis
- LOG_LEVEL=debug
volumes:
- ./src:/app/src # Hot reload en desarrollo
depends_on:
qdrant:
condition: service_healthy
redis:
condition: service_healthy
deploy:
resources:
limits:
memory: 2G
# ── Vector Database ─────────────────────────────────
qdrant:
image: qdrant/qdrant:v1.9
ports:
- "6333:6333"
volumes:
- qdrant_data:/qdrant/storage
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:6333/healthz"]
interval: 10s
timeout: 5s
retries: 3
# ── Cache ───────────────────────────────────────────
redis:
image: redis:7-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
# ── Observabilidad ──────────────────────────────────
langfuse:
image: langfuse/langfuse:2
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://langfuse:pass@postgres:5432/langfuse
depends_on:
- postgres
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: langfuse
POSTGRES_PASSWORD: pass
POSTGRES_DB: langfuse
volumes:
- pg_data:/var/lib/postgresql/data
volumes:
qdrant_data:
pg_data:
Kubernetes: Deployment para IA
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: rag-api
labels:
app: rag-api
spec:
replicas: 3
selector:
matchLabels:
app: rag-api
template:
metadata:
labels:
app: rag-api
spec:
containers:
- name: rag-api
image: 123456789.dkr.ecr.us-east-1.amazonaws.com/rag-api:latest
ports:
- containerPort: 8000
# ── Configuración desde Secrets/ConfigMaps ──
envFrom:
- configMapRef:
name: rag-config
- secretRef:
name: rag-secrets
# ── Recursos: importante para IA ────────────
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
# ── Health checks ───────────────────────────
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 15
---
# HPA para escalar según latencia
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: rag-api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: rag-api
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
# Escalar basado en request queue depth
- type: Pods
pods:
metric:
name: http_request_queue_length
target:
type: AverageValue
averageValue: "10"
Service y Ingress
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: rag-api
spec:
selector:
app: rag-api
ports:
- port: 80
targetPort: 8000
type: ClusterIP
---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rag-api
annotations:
kubernetes.io/ingress.class: "alb"
alb.ingress.kubernetes.io/scheme: "internet-facing"
alb.ingress.kubernetes.io/target-type: "ip"
alb.ingress.kubernetes.io/healthcheck-path: "/health"
# Rate limiting
nginx.ingress.kubernetes.io/limit-rps: "50"
spec:
rules:
- host: api.ai-platform.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: rag-api
port:
number: 80
GPU en Kubernetes
# k8s/gpu-deployment.yaml — Pod con GPU para embedding local
apiVersion: apps/v1
kind: Deployment
metadata:
name: embedding-service
spec:
replicas: 2
template:
spec:
containers:
- name: embedder
image: embedding-service:latest
resources:
limits:
# Solicita 1 GPU dedicada; el scheduler de K8s
# coloca el pod únicamente en un nodo con GPU libre
nvidia.com/gpu: 1
memory: "16Gi"
# ── Persistir pesos del modelo ────────────────
# Monta un volumen persistente para cachear los
# pesos descargados. Evita re-descargar ~GBs de
# modelo cada vez que el pod se reinicia.
volumeMounts:
- name: model-cache
mountPath: /models
volumes:
- name: model-cache
# PersistentVolumeClaim: almacenamiento que
# sobrevive a reinicios y re-schedulings del pod
persistentVolumeClaim:
claimName: model-cache-pvc
# ── Restringir a nodos con GPU específica ───────
# nodeSelector filtra nodos por etiqueta; aquí solo
# nodos con Tesla T4 son elegibles para este pod
nodeSelector:
accelerator: nvidia-tesla-t4
# ── Toleraciones para nodos GPU ─────────────────
# Los nodos con GPU suelen tener un "taint" que
# impide que workloads no-GPU se programen ahí.
# Esta toleration permite que ESTE pod sí se
# programe en esos nodos reservados.
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
Helm Chart Básico
# Crear chart para la aplicación de IA
helm create rag-api
# Estructura generada:
# rag-api/
# ├── Chart.yaml
# ├── values.yaml ← Configuración
# ├── templates/
# │ ├── deployment.yaml
# │ ├── service.yaml
# │ ├── ingress.yaml
# │ └── hpa.yaml
# └── charts/ ← Dependencias
# values.yaml — Configuración del chart
replicaCount: 3
image:
repository: 123456789.dkr.ecr.us-east-1.amazonaws.com/rag-api
tag: "latest"
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "2"
memory: 4Gi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 20
targetCPU: 70
# Configuración específica de IA
ai:
modelProvider: bedrock
embeddingModel: amazon.titan-embed-text-v2
vectorDB: qdrant
cacheEnabled: true
cacheTTL: 3600
Resumen
| Concepto | Herramienta | Uso en IA |
|---|---|---|
| Container | Docker | Empaquetar app + dependencias ML |
| Orquestación | Kubernetes | Escalar API + GPU workloads |
| GPU | NVIDIA plugin | Embedding local, inferencia |
| Autoscaling | HPA | Escalar según CPU, latencia, queue |
| Configuration | ConfigMap/Secret | API keys, model config |
| Package Manager | Helm | Despliegues reproducibles |
🧠 Preguntas de Repaso
1. ¿Por qué se usa un Dockerfile multi-stage para aplicaciones de IA?
- A) Para soportar múltiples lenguajes de programación
- B) Para separar la fase de build (con compiladores gcc/g++) de la fase runtime, resultando en imágenes más pequeñas y seguras sin herramientas de compilación
- C) Para ejecutar múltiples procesos en un solo container
- D) Para soportar diferentes arquitecturas de CPU
Respuesta: B) — Multi-stage separa el build (stage 1: instala gcc/g++, compila dependencias) del runtime (stage 2: solo copia las dependencias compiladas y el código). La imagen final es más pequeña (sin compiladores) y más segura (menor superficie de ataque).
2. En Kubernetes, ¿cuál es la diferencia entre readinessProbe y livenessProbe?
- A) readinessProbe verifica si el pod puede recibir tráfico (15s initial, 10s period); livenessProbe verifica si el proceso está vivo (30s initial, 15s period) y lo reinicia si falla
- B) Son lo mismo pero con nombres diferentes
- C) readinessProbe es para pods en desarrollo, livenessProbe para producción
- D) readinessProbe verifica la CPU, livenessProbe verifica la memoria
Respuesta: A) — readinessProbe determina cuándo un pod está listo para recibir tráfico (si falla, se remueve del Service). livenessProbe verifica que el proceso siga vivo (si falla, Kubernetes reinicia el container).
3. Para solicitar GPUs en Kubernetes, ¿qué configuración es necesaria?
- A) Solo agregar
nodeSelector: gpu=true - B) Especificar
nvidia.com/gpu: 1en limits, usar PersistentVolumeClaim para caché de modelos, nodeSelector para nodos con GPU, y tolerations para el taint NoSchedule de nodos GPU - C) Instalar CUDA directamente en el contenedor
- D) Usar un Deployment especial tipo "GPUDeployment"
Respuesta: B) — Se necesita: (1)
nvidia.com/gpu: 1en resources.limits, (2) PVC para caché de modelos que persista entre reinicios, (3) nodeSelector para restricir a nodos con GPU (ej: nvidia-tesla-t4), (4) tolerations para permitir scheduling en nodos con taint NoSchedule.
4. ¿Qué hace el HPA (HorizontalPodAutoscaler) en un deployment de IA?
- A) Escala verticalmente agregando más CPU y memoria a cada pod
- B) Escala horizontalmente entre un mínimo y máximo de réplicas basándose en métricas como CPU (70%) y longitud de cola de requests
- C) Escala el clúster de Kubernetes agregando más nodos
- D) Balancea el tráfico entre pods existentes
Respuesta: B) — HPA escala horizontalmente (agrega/remueve pods) entre minReplicas y maxReplicas basándose en métricas: CPU utilization target 70% y métricas custom como
http_request_queue_lengthcon valor promedio de 10.