Deployment y Escalabilidad
Capítulo 24: Deployment y Escalabilidad
Una de las mayores ventajas de CQRS es la capacidad de escalar lectura y escritura independientemente. El Read Side típicamente recibe mucho más tráfico que el Write Side, por lo que tener más réplicas de queries que de comandos optimiza recursos.
Componentes de un Sistema CQRS
Un deployment CQRS típico incluye:
- Command API: Recibe y procesa comandos (pocas réplicas)
- Query API: Responde consultas (muchas réplicas)
- Projection Workers: Procesan eventos y actualizan read models
- Bases de datos: PostgreSQL (write), Elasticsearch (read), Redis (cache)
- Message Broker: RabbitMQ o Kafka para eventos
Arquitectura de Deployment
Este diagrama muestra cómo fluyen los datos: comandos entran por Command API, generan eventos que van al Message Broker, los Projection Workers los procesan y actualizan Elasticsearch, y Query API lee de ahí.
Load Balancer
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ Command API (x2) │ │ Query API (x5) │
│ - CreateOrder │ │ - GetOrder │
│ - AddItem │ │ - ListOrders │
└─────────────────────────┘ └─────────────────────────┘
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ PostgreSQL Primary │ │ Elasticsearch Cluster │
│ (Write Database) │ │ (Read Database) │
└─────────────────────────┘ └─────────────────────────┘
│ ▲
▼ │
┌─────────────────────────┐ ┌─────────────────────────┐
│ Message Broker │───▶│ Projection Workers │
│ (RabbitMQ/Kafka) │ │ (x3) │
└─────────────────────────┘ └─────────────────────────┘
Docker Compose
Docker Compose permite definir y ejecutar aplicaciones multi-contenedor. Cada servicio tiene su propia imagen, variables de entorno y configuración de réplicas.
deploy.replicas en Docker Compose Swarm mode indica cuántas instancias ejecutar de cada servicio.
# docker-compose.yml
services:
command-api:
build:
context: .
dockerfile: Dockerfile.command
environment:
DATABASE_URL: postgres://user:pass@postgres:5432/orderflow
RABBITMQ_URL: amqp://rabbitmq:5672
deploy:
replicas: 2
query-api:
build:
context: .
dockerfile: Dockerfile.query
environment:
ELASTICSEARCH_URL: http://elasticsearch:9200
REDIS_URL: redis://redis:6379
deploy:
replicas: 5
projection-worker:
build:
context: .
dockerfile: Dockerfile.projection
environment:
RABBITMQ_URL: amqp://rabbitmq:5672
ELASTICSEARCH_URL: http://elasticsearch:9200
deploy:
replicas: 3
postgres:
image: postgres:16-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
elasticsearch:
image: elasticsearch:8.11.0
environment:
- discovery.type=single-node
redis:
image: redis:7-alpine
rabbitmq:
image: rabbitmq:3-management
Kubernetes Manifests
Kubernetes es el orquestador de contenedores más usado. Los Deployments definen qué contenedores ejecutar y cuántas réplicas.
resources.requests indica los recursos mínimos que el pod necesita para ser schedulado. limits define el máximo que puede consumir.
secretKeyRef obtiene valores sensibles de Kubernetes Secrets, evitando credenciales en el código.
# k8s/command-api.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: command-api
spec:
replicas: 2
selector:
matchLabels:
app: command-api
template:
metadata:
labels:
app: command-api
spec:
containers:
- name: command-api
image: orderflow/command-api:latest
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "1Gi"
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: command-api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: command-api
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
El HorizontalPodAutoscaler (HPA) ajusta automáticamente el número de réplicas basándose en métricas. Cuando CPU supera el umbral, crea más pods; cuando baja, los elimina.
La Query API tiene límites más agresivos (60% CPU, hasta 50 réplicas) porque típicamente recibe más tráfico.
# k8s/query-api.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: query-api
spec:
replicas: 5
selector:
matchLabels:
app: query-api
template:
spec:
containers:
- name: query-api
image: orderflow/query-api:latest
resources:
requests:
cpu: "250m"
memory: "256Mi"
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: query-api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: query-api
minReplicas: 5
maxReplicas: 50
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
Estrategias de Escalado
Cada componente tiene diferentes necesidades de escalado:
| Componente | Estrategia | Trigger |
|---|---|---|
| Command API | Horizontal | CPU > 70% |
| Query API | Horizontal | CPU > 60%, RPS |
| Projections | Horizontal | Event lag > 1000 |
| PostgreSQL | Vertical + Read Replicas | Connections |
| Elasticsearch | Horizontal (shards) | Storage, queries/s |
| Redis | Cluster mode | Memory, ops/s |
Migración Zero-Downtime
Zero-downtime migration permite actualizar el Read Side sin interrumpir el servicio. La técnica de alias swap crea un nuevo índice, lo llena, y luego cambia el alias atómicamente.
Pasos:
- Pausar proyecciones para evitar escrituras durante la migración
- Crear nuevo índice con el nuevo schema
- Reconstruir desde eventos (pueden ser millones)
- Verificar que el nuevo índice tiene los mismos datos
- Cambiar el alias atómicamente (las queries no notan el cambio)
- Reanudar proyecciones apuntando al nuevo índice
// scripts/migrate-projections.ts
async function migrateProjections(): Promise<void> {
const projectionManager = new ProjectionManager();
// 1. Pausar proyecciones actuales
await projectionManager.pauseAll();
// 2. Crear nuevo índice
await elastic.indices.create({
index: 'orders-v2',
body: newMapping
});
// 3. Reconstruir desde eventos
await projectionManager.rebuild('order-summary', {
targetIndex: 'orders-v2'
});
// 4. Verificar consistencia
const isConsistent = await verifyConsistency('orders', 'orders-v2');
if (!isConsistent) throw new Error('Migration failed');
// 5. Swap aliases
await elastic.indices.updateAliases({
body: {
actions: [
{ remove: { index: 'orders-v1', alias: 'orders' } },
{ add: { index: 'orders-v2', alias: 'orders' } }
]
}
});
// 6. Reanudar proyecciones
await projectionManager.resumeAll();
}
Configuración por Ambiente
Diferentes ambientes requieren diferentes configuraciones. Development usa recursos mínimos para desarrollo local. Production tiene auto-scaling con límites amplios.
// src/config/index.ts
export const config = {
development: {
commandApi: { replicas: 1 },
queryApi: { replicas: 1 },
projections: { replicas: 1 }
},
staging: {
commandApi: { replicas: 2 },
queryApi: { replicas: 3 },
projections: { replicas: 2 }
},
production: {
commandApi: { replicas: 3, minReplicas: 2, maxReplicas: 10 },
queryApi: { replicas: 5, minReplicas: 5, maxReplicas: 50 },
projections: { replicas: 3, minReplicas: 3, maxReplicas: 10 }
}
};
CI/CD Pipeline
GitHub Actions automatiza el build y deploy. Cada push a main:
- Construye las imágenes Docker con el SHA del commit como tag
- Las sube al registry
- Actualiza los deployments en Kubernetes
- Espera a que el rollout complete
kubectl rollout status bloquea hasta que todos los pods estén healthy, garantizando que el deploy fue exitoso.
# .github/workflows/deploy.yml
name: Deploy CQRS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build images
run: |
docker build -f Dockerfile.command -t orderflow/command-api:${{ github.sha }} .
docker build -f Dockerfile.query -t orderflow/query-api:${{ github.sha }} .
docker build -f Dockerfile.projection -t orderflow/projection:${{ github.sha }} .
- name: Push images
run: |
docker push orderflow/command-api:${{ github.sha }}
docker push orderflow/query-api:${{ github.sha }}
docker push orderflow/projection:${{ github.sha }}
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/command-api command-api=orderflow/command-api:${{ github.sha }}
kubectl set image deployment/query-api query-api=orderflow/query-api:${{ github.sha }}
kubectl set image deployment/projection projection=orderflow/projection:${{ github.sha }}
kubectl rollout status deployment/command-api
kubectl rollout status deployment/query-api
Resumen
Deployment CQRS:
- Escalar read side independiente del write side
- Proyecciones como workers separados
- HPA basado en métricas específicas de cada componente
- Migraciones zero-downtime con reconstrucción de proyecciones
Glosario
Kubernetes
Definición: Plataforma de orquestación de contenedores de código abierto que automatiza el deployment, escalado y gestión de aplicaciones containerizadas.
Por qué es importante: Gestiona automáticamente réplicas, balanceo de carga, recuperación de fallos, y rolling updates. Estándar de la industria para producción.
Ejemplo práctico: Un Deployment con replicas: 5 asegura que siempre haya 5 pods corriendo. Si uno falla, Kubernetes lo reemplaza automáticamente.
HorizontalPodAutoscaler (HPA)
Definición: Recurso de Kubernetes que ajusta automáticamente el número de pods basándose en métricas observadas (CPU, memoria, custom metrics).
Por qué es importante: Permite que la aplicación se adapte a cambios en la carga sin intervención manual. Optimiza costos escalando hacia abajo cuando hay poco tráfico.
Ejemplo práctico: Si CPU promedio supera 70%, HPA crea más pods (hasta maxReplicas). Si baja de 50%, elimina pods (hasta minReplicas).
Docker Compose
Definición: Herramienta para definir y ejecutar aplicaciones Docker multi-contenedor usando un archivo YAML declarativo.
Por qué es importante: Simplifica el desarrollo local de sistemas con múltiples servicios. Un comando levanta toda la infraestructura.
Ejemplo práctico: docker-compose up inicia API, base de datos, Redis, y RabbitMQ configurados para comunicarse entre sí.
Rolling Update
Definición: Estrategia de deployment donde los pods se actualizan gradualmente, manteniendo siempre un número mínimo disponible.
Por qué es importante: Permite actualizaciones sin downtime. Si la nueva versión falla, solo afecta a algunos pods mientras Kubernetes revierte.
Ejemplo práctico: Con 5 réplicas, Kubernetes puede actualizar 2 a la vez, esperando que estén healthy antes de continuar con las siguientes.
Alias (Elasticsearch)
Definición: Nombre alternativo para uno o más índices de Elasticsearch. Las queries usan el alias, que puede apuntar a diferentes índices.
Por qué es importante: Permite cambiar qué índice sirve las queries sin modificar la aplicación. Esencial para migraciones zero-downtime.
Ejemplo práctico: El alias orders apunta a orders-v1. Durante migración, se crea orders-v2, se llena, y se cambia el alias atómicamente.
Zero-Downtime Deployment
Definición: Técnica para actualizar una aplicación sin interrumpir el servicio a los usuarios.
Por qué es importante: Los usuarios no experimentan errores ni tiempo de espera durante deploys. Permite deployar frecuentemente con confianza.
Ejemplo práctico: Rolling update actualiza pods gradualmente. Migraciones de schema usan alias swap. Blue-green tiene dos ambientes y cambia tráfico.
Message Broker
Definición: Sistema intermediario que recibe, almacena y enruta mensajes entre productores y consumidores. Ejemplos: RabbitMQ, Kafka.
Por qué es importante: Desacopla productores de consumidores. Si un consumidor está caído, los mensajes esperan. Permite replay de eventos.
Ejemplo práctico: Command API publica eventos a RabbitMQ. Projection Workers consumen esos eventos a su propio ritmo, sin afectar al productor.
Vertical Scaling
Definición: Aumentar recursos (CPU, RAM) de una máquina/pod existente. Opuesto a horizontal scaling que agrega más instancias.
Por qué es importante: Algunas cargas de trabajo (bases de datos) escalan mejor verticalmente. Más simple que distribuir datos entre nodos.
Ejemplo práctico: PostgreSQL puede manejar más conexiones aumentando RAM. Es más simple que configurar read replicas o sharding.
CI/CD Pipeline
Definición: Proceso automatizado que compila, prueba, y despliega código automáticamente cuando se hace push al repositorio.
Por qué es importante: Elimina pasos manuales propensos a errores. Garantiza que todo deploy pase las mismas verificaciones.
Ejemplo práctico: Push a main dispara: build de imagen Docker, push a registry, kubectl apply, espera a que pods estén healthy.
Projection Worker
Definición: Proceso dedicado que consume eventos del message broker y actualiza los read models (proyecciones).
Por qué es importante: Separa el procesamiento de eventos de las APIs. Puede escalarse independientemente según el lag de eventos.
Ejemplo práctico: 3 projection workers procesan eventos en paralelo. Si el lag crece, se agregan más workers para aumentar throughput.