← Volver al listado de tecnologías

Deployment y Escalabilidad

Por: SiempreListo
cqrsdeploymentkubernetesdockerescalabilidad

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:

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:

ComponenteEstrategiaTrigger
Command APIHorizontalCPU > 70%
Query APIHorizontalCPU > 60%, RPS
ProjectionsHorizontalEvent lag > 1000
PostgreSQLVertical + Read ReplicasConnections
ElasticsearchHorizontal (shards)Storage, queries/s
RedisCluster modeMemory, 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:

  1. Pausar proyecciones para evitar escrituras durante la migración
  2. Crear nuevo índice con el nuevo schema
  3. Reconstruir desde eventos (pueden ser millones)
  4. Verificar que el nuevo índice tiene los mismos datos
  5. Cambiar el alias atómicamente (las queries no notan el cambio)
  6. 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:

  1. Construye las imágenes Docker con el SHA del commit como tag
  2. Las sube al registry
  3. Actualiza los deployments en Kubernetes
  4. 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:

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.


← Capítulo 23: Monitoreo | Volver al Índice →