Capítulo 23: Deployment con Docker y Kubernetes
Capítulo 23: Deployment con Docker y Kubernetes
“Un sistema Event Sourced requiere consideraciones especiales para producción”
Consideraciones Especiales para Event Sourcing
Desplegar sistemas Event Sourced presenta desafíos únicos:
- Projection Workers: Deben ser singleton para evitar procesamiento duplicado
- Event Store: Es crítico; requiere backups especiales
- Migraciones: Los eventos son inmutables; el schema evoluciona de forma diferente
- Consistencia Eventual: Las proyecciones pueden estar desactualizadas durante deploys
Este capítulo cubre la configuración de producción con Docker y Kubernetes.
Docker
Dockerfile Multi-stage
Un Dockerfile multi-stage separa la construcción de la imagen final:
- Stage builder: Incluye todas las dependencias de desarrollo y compila
- Stage final: Solo incluye artefactos compilados y runtime
Esto reduce el tamaño de imagen y superficie de ataque.
# Dockerfile
FROM oven/bun:1.0 AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
# Production image
FROM oven/bun:1.0-slim
WORKDIR /app
# Non-root user
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 orderflow
USER orderflow
COPY --from=builder --chown=orderflow:nodejs /app/dist ./dist
COPY --from=builder --chown=orderflow:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=orderflow:nodejs /app/package.json ./
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health/live || exit 1
CMD ["bun", "run", "dist/server.js"]
Docker Compose Producción
Para producción simple o staging, Docker Compose orquesta múltiples contenedores. Notas importantes:
- depends_on con condition: Espera que dependencias estén healthy antes de arrancar
- deploy.replicas: Escala horizontalmente la API
- projection-worker replicas: 1: CRÍTICO - solo una instancia para evitar duplicados
# docker-compose.prod.yml
services:
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://orderflow:${DB_PASSWORD}@postgres:5432/orderflow
REDIS_URL: redis://redis:6379
NODE_ENV: production
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
deploy:
replicas: 3
resources:
limits:
cpus: '1'
memory: 512M
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health/live"]
interval: 30s
timeout: 10s
retries: 3
projection-worker:
build:
context: .
dockerfile: Dockerfile.worker
environment:
DATABASE_URL: postgres://orderflow:${DB_PASSWORD}@postgres:5432/orderflow
depends_on:
- postgres
deploy:
replicas: 1 # Solo una instancia para evitar duplicados
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: orderflow
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: orderflow
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U orderflow"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
redis_data:
Kubernetes
Kubernetes es el estándar para orquestación de contenedores en producción. Los conceptos clave son:
- Deployment: Define pods replicados con rollouts automáticos
- StatefulSet: Para workloads que requieren identidad persistente (projection workers)
- Service: Expone pods internamente o externamente
- Ingress: Gestiona tráfico HTTP/HTTPS externo
Deployment API
# k8s/api-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: orderflow-api
labels:
app: orderflow
component: api
spec:
replicas: 3
selector:
matchLabels:
app: orderflow
component: api
template:
metadata:
labels:
app: orderflow
component: api
spec:
containers:
- name: api
image: orderflow/api:latest
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: orderflow-secrets
key: database-url
- name: NODE_ENV
value: "production"
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1000m"
memory: "512Mi"
readinessProbe:
httpGet:
path: /health/ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health/live
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: orderflow-api
spec:
selector:
app: orderflow
component: api
ports:
- port: 80
targetPort: 3000
type: ClusterIP
Projection Worker (StatefulSet)
El projection worker usa StatefulSet en lugar de Deployment porque:
- Una sola réplica: Evita procesamiento duplicado de eventos
- Identidad estable: El pod siempre tiene el mismo nombre (útil para logging)
- Ordenamiento: Kubernetes espera que el pod anterior termine antes de crear el nuevo
# k8s/projection-worker.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: orderflow-projection-worker
spec:
serviceName: projection-worker
replicas: 1 # Una sola réplica para evitar procesamiento duplicado
selector:
matchLabels:
app: orderflow
component: projection-worker
template:
metadata:
labels:
app: orderflow
component: projection-worker
spec:
containers:
- name: worker
image: orderflow/worker:latest
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: orderflow-secrets
key: database-url
- name: WORKER_ID
valueFrom:
fieldRef:
fieldPath: metadata.name
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
ConfigMap y Secrets
ConfigMap almacena configuración no sensible; Secret almacena credenciales cifradas. Ambos se inyectan como variables de entorno o archivos.
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: orderflow-config
data:
LOG_LEVEL: "info"
PROJECTION_POLL_INTERVAL: "100"
SNAPSHOT_INTERVAL: "100"
---
# k8s/secrets.yaml (usar sealed-secrets en producción)
apiVersion: v1
kind: Secret
metadata:
name: orderflow-secrets
type: Opaque
stringData:
database-url: postgres://orderflow:password@postgres:5432/orderflow
Ingress
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: orderflow-ingress
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
tls:
- hosts:
- api.orderflow.example.com
secretName: orderflow-tls
rules:
- host: api.orderflow.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: orderflow-api
port:
number: 80
HorizontalPodAutoscaler
El HPA escala automáticamente el número de réplicas basándose en métricas (CPU, memoria, o métricas custom). Solo aplica a la API, no al projection worker (que debe ser singleton).
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: orderflow-api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: orderflow-api
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
CI/CD Pipeline
Un pipeline de CI/CD automatiza el proceso de test, build y deploy. Las etapas típicas son:
- Test: Ejecutar tests unitarios y de integración
- Build: Construir imagen Docker y pushear al registry
- Deploy: Actualizar Kubernetes con la nueva imagen
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- run: bun install
- run: bun test
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
push: true
tags: |
ghcr.io/${{ github.repository }}/api:${{ github.sha }}
ghcr.io/${{ github.repository }}/api:latest
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/orderflow-api \
api=ghcr.io/${{ github.repository }}/api:${{ github.sha }}
kubectl rollout status deployment/orderflow-api
Consideraciones de Producción
Backup de Event Store
El Event Store es la fuente de verdad del sistema. Perder eventos significa perder historia irrecuperable. Los backups deben:
- Ejecutarse frecuentemente (al menos cada hora)
- Verificarse periódicamente restaurando en ambiente de prueba
- Almacenarse en ubicación geográfica diferente
#!/bin/bash
# backup-events.sh
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="events_backup_${DATE}.sql"
pg_dump -h $DB_HOST -U orderflow \
-t events \
--data-only \
-f $BACKUP_FILE
# Comprimir y subir a S3
gzip $BACKUP_FILE
aws s3 cp ${BACKUP_FILE}.gz s3://backups/orderflow/
Migración de Base de Datos
Las migraciones crean y modifican el schema de las tablas. En Event Sourcing:
- La tabla de eventos raramente cambia (append-only)
- Las tablas de proyecciones pueden recrearse desde eventos
- Los índices son críticos para rendimiento de consultas
// migrations/001_initial.ts
export async function up(db: Database): Promise<void> {
await db.execute(sql`
CREATE TABLE IF NOT EXISTS events (
global_position BIGSERIAL PRIMARY KEY,
stream_id VARCHAR(255) NOT NULL,
stream_position INT NOT NULL,
event_type VARCHAR(255) NOT NULL,
data JSONB NOT NULL,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(stream_id, stream_position)
)
`);
await db.execute(sql`
CREATE INDEX idx_events_stream ON events(stream_id, stream_position)
`);
}
export async function down(db: Database): Promise<void> {
await db.execute(sql`DROP TABLE IF EXISTS events`);
}
Resumen
- Docker multi-stage reduce el tamaño de imagen
- Kubernetes gestiona escalado y disponibilidad
- Los projection workers deben ser singleton
- El Event Store requiere backups especiales
- El CI/CD automatiza despliegues seguros
Glosario
Multi-stage Build
Definición: Técnica de Docker donde el Dockerfile tiene múltiples stages; solo el contenido del stage final se incluye en la imagen.
Por qué es importante: Reduce tamaño de imagen dramáticamente (de ~1GB con node_modules de desarrollo a ~100MB solo con producción).
Ejemplo práctico: Stage “builder” instala devDependencies y compila TypeScript; stage “final” copia solo dist/ y node_modules de producción.
StatefulSet
Definición: Recurso de Kubernetes para workloads que requieren identidad de red estable, almacenamiento persistente, y ordenamiento de pods.
Por qué es importante: Los projection workers necesitan ejecutarse como singleton; StatefulSet garantiza que solo hay una instancia activa.
Ejemplo práctico: orderflow-projection-worker-0 siempre es el mismo pod; si muere, Kubernetes lo recrea con el mismo nombre antes de crear -1.
Deployment vs StatefulSet
Definición: Deployment maneja pods intercambiables; StatefulSet maneja pods con identidad persistente.
Por qué es importante: La API puede escalar con Deployment (pods intercambiables); projection worker usa StatefulSet (singleton con identidad).
Ejemplo práctico: API: orderflow-api-7b8c9-xyz, orderflow-api-7b8c9-abc (nombres aleatorios). Worker: orderflow-projection-worker-0 (nombre fijo).
Readiness Probe
Definición: Verificación periódica que Kubernetes ejecuta para determinar si un pod puede recibir tráfico.
Por qué es importante: Un pod que arrancó pero aún carga datos no debe recibir requests; el probe previene esto.
Ejemplo práctico: /health/ready verifica que la proyección está actualizada (lag < 1000); hasta entonces, el pod no recibe tráfico del Service.
Liveness Probe
Definición: Verificación periódica que determina si un pod está vivo; si falla, Kubernetes lo reinicia.
Por qué es importante: Detecta procesos que se colgaron pero no murieron; el reinicio automático recupera el servicio.
Ejemplo práctico: /health/live devuelve 200 si el proceso responde; si tarda más de 10s o devuelve error 3 veces seguidas, Kubernetes mata y recrea el pod.
HorizontalPodAutoscaler (HPA)
Definición: Recurso de Kubernetes que ajusta automáticamente el número de réplicas basándose en métricas.
Por qué es importante: Escala la API según demanda real; durante picos crea más pods, en calma los reduce (ahorra costos).
Ejemplo práctico: CPU promedio > 70% → HPA crea más réplicas hasta maxReplicas. CPU < 50% → reduce réplicas hasta minReplicas.
ConfigMap
Definición: Recurso de Kubernetes para almacenar configuración no sensible como pares clave-valor o archivos.
Por qué es importante: Separa configuración del código; permite cambiar comportamiento sin reconstruir imagen.
Ejemplo práctico: LOG_LEVEL: "info", SNAPSHOT_INTERVAL: "100" se inyectan como variables de entorno al pod.
Secret
Definición: Recurso de Kubernetes para almacenar datos sensibles (passwords, tokens) cifrados en base64.
Por qué es importante: Las credenciales no deben estar en ConfigMaps (visibles) ni en imágenes Docker (versionadas).
Ejemplo práctico: database-url: postgres://user:password@host/db se almacena como Secret y se inyecta como variable de entorno.
Rolling Update
Definición: Estrategia de deployment donde los pods se actualizan gradualmente, manteniendo disponibilidad durante el proceso.
Por qué es importante: Permite deployments sin downtime; si el nuevo pod falla, Kubernetes detiene el rollout automáticamente.
Ejemplo práctico: Con 3 réplicas, Kubernetes crea 1 pod nuevo, espera que esté ready, termina 1 pod viejo, repite hasta actualizar los 3.
Ingress
Definición: Recurso de Kubernetes que gestiona acceso HTTP/HTTPS externo, con routing basado en host y path.
Por qué es importante: Centraliza TLS termination, routing, y load balancing para múltiples servicios bajo un solo dominio.
Ejemplo práctico: api.orderflow.com/orders → Service orderflow-api; api.orderflow.com/metrics → Service prometheus.