DevOps

Despliegue Canary en Kubernetes con rollback automático mediante CI/CD y métricas de Prometheus

Ruslan Ismailov Publicado 12 min de lectura
D

Introducción: qué es el despliegue canary y para qué sirve

El canary deployment (despliegue canario) es una estrategia de lanzamiento de nuevas versiones de una aplicación en la que un pequeño porcentaje del tráfico real se dirige a la nueva versión, mientras la mayor parte de la carga permanece en la versión estable. El nombre hace referencia a la práctica de los mineros, que llevaban canarios a la mina: si el pájaro moría, había gas en el aire. De forma análoga, si la nueva versión del servicio falla con el 5% del tráfico, no llegará al 100% de los usuarios.

Cómo se diferencia el canary de otras estrategias de despliegue:

  • Rolling update — reemplaza gradualmente los pods de la versión antigua por los nuevos; el tráfico se distribuye a medida que avanza el reemplazo. No permite controlar con precisión el porcentaje de tráfico hacia la nueva versión.
  • Blue-green — mantiene dos copias completas del entorno y el cambio se realiza de golpe. Costoso en recursos y sin transición gradual.
  • Canary — ofrece control preciso sobre la fracción de tráfico, permite validar métricas antes de la transición completa y realizar un rollback automático ante problemas.

El despliegue canary es especialmente relevante en arquitecturas de microservicios, donde los servicios se despliegan de forma independiente y el coste de un error en producción es elevado. Combinado con Kubernetes, pipelines de CI/CD y Prometheus, se convierte en un proceso completamente automatizado de despliegue seguro.

Arquitectura del despliegue canary en Kubernetes

La arquitectura estándar incluye tres componentes: dos objetos Deployment (stable y canary), un Service y un Ingress con enrutamiento por pesos.

Deployment: stable y canary

Se ejecutan dos Deployments con etiquetas diferentes (track: stable y track: canary), pero con el mismo label de aplicación. El Service selecciona los pods por el label común, y la distribución ponderada del tráfico se configura a nivel del Ingress.

Enrutamiento por pesos mediante NGINX Ingress

El NGINX Ingress Controller soporta canary a través de anotaciones. Un recurso Ingress independiente se marca como canary y recibe un peso (por ejemplo, 10%). La Gateway API (el nuevo estándar de Kubernetes) ofrece una forma más expresiva mediante el recurso HTTPRoute con indicación explícita de los pesos de los backends.

Configuración paso a paso del canary mediante manifiestos de Kubernetes

Paso 1: Stable Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: go-service-stable
  labels:
    app: go-service
    track: stable
spec:
  replicas: 4
  selector:
    matchLabels:
      app: go-service
      track: stable
  template:
    metadata:
      labels:
        app: go-service
        track: stable
    spec:
      containers:
      - name: go-service
        image: registry.example.com/go-service:v1.4.2
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "500m"
            memory: "256Mi"

Paso 2: Canary Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: go-service-canary
  labels:
    app: go-service
    track: canary
spec:
  replicas: 1
  selector:
    matchLabels:
      app: go-service
      track: canary
  template:
    metadata:
      labels:
        app: go-service
        track: canary
    spec:
      containers:
      - name: go-service
        image: registry.example.com/go-service:v1.5.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "500m"
            memory: "256Mi"

Paso 3: Service

apiVersion: v1
kind: Service
metadata:
  name: go-service
spec:
  selector:
    app: go-service
  ports:
  - port: 80
    targetPort: 8080

El Service selecciona los pods por el label app: go-service, es decir, tanto los pods stable como los canary. Con 4 réplicas stable + 1 canary, la carga se distribuirá aproximadamente 80/20. Para un control preciso utilizamos el Ingress.

Paso 4: Ingress con enrutamiento por pesos

# Ingress principal (stable)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: go-service-stable
spec:
  ingressClassName: nginx
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: go-service-stable-svc
            port:
              number: 80
---
# Canary Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: go-service-canary
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
  ingressClassName: nginx
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: go-service-canary-svc
            port:
              number: 80

La anotación nginx.ingress.kubernetes.io/canary-weight: "10" dirige el 10% del tráfico a la versión canary. El valor puede modificarse dinámicamente mediante kubectl annotate sin necesidad de recrear el recurso.

Integración con el pipeline de CI/CD: GitHub Actions

A continuación se muestra un ejemplo de workflow de GitHub Actions que implementa el despliegue canary con incremento progresivo del tráfico y verificación de métricas en cada etapa.

name: Canary Deploy

on:
  push:
    branches: [main]

env:
  IMAGE: registry.example.com/go-service
  NAMESPACE: production

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build and push image
        run: |
          docker build -t $IMAGE:${{ github.sha }} .
          docker push $IMAGE:${{ github.sha }}

  canary-deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure kubectl
        uses: azure/setup-kubectl@v3

      - name: Deploy canary (10%)
        run: |
          kubectl set image deployment/go-service-canary \
            go-service=$IMAGE:${{ github.sha }} \
            -n $NAMESPACE
          kubectl annotate ingress go-service-canary \
            nginx.ingress.kubernetes.io/canary-weight=10 \
            --overwrite -n $NAMESPACE
          kubectl rollout status deployment/go-service-canary -n $NAMESPACE

      - name: Wait and check metrics (10%)
        run: bash scripts/check_metrics.sh 10

      - name: Increase to 30%
        run: |
          kubectl annotate ingress go-service-canary \
            nginx.ingress.kubernetes.io/canary-weight=30 \
            --overwrite -n $NAMESPACE

      - name: Wait and check metrics (30%)
        run: bash scripts/check_metrics.sh 30

      - name: Full rollout (100%)
        run: |
          kubectl set image deployment/go-service-stable \
            go-service=$IMAGE:${{ github.sha }} \
            -n $NAMESPACE
          kubectl rollout status deployment/go-service-stable -n $NAMESPACE
          kubectl annotate ingress go-service-canary \
            nginx.ingress.kubernetes.io/canary-weight=0 \
            --overwrite -n $NAMESPACE
          kubectl scale deployment/go-service-canary --replicas=0 -n $NAMESPACE

Monitorización de métricas con Prometheus

Prometheus es la herramienta clave para evaluar la calidad de una release canary. Configuramos dos criterios de éxito principales: tasa de errores y latencia (p99).

Configuración del ServiceMonitor para Prometheus Operator

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: go-service-canary
  namespace: production
spec:
  selector:
    matchLabels:
      track: canary
  endpoints:
  - port: http
    path: /metrics
    interval: 15s

Consultas PromQL para evaluar la calidad

Tasa de errores (5xx en los últimos 5 minutos):

sum(rate(http_requests_total{job="go-service-canary",status=~"5.."}[5m]))
/
sum(rate(http_requests_total{job="go-service-canary"}[5m]))

Latencia P99:

histogram_quantile(0.99,
  sum(rate(http_request_duration_seconds_bucket{job="go-service-canary"}[5m]))
  by (le)
)

Rollback automático al superar los umbrales de error

El script check_metrics.sh consulta la API de Prometheus e inicia el rollback si las métricas superan los límites permitidos.

#!/bin/bash
# check_metrics.sh
# Argumento: peso actual del canary (para logging)
CANARY_WEIGHT=$1
PROMETHEUS_URL="http://prometheus.monitoring.svc.cluster.local:9090"
ERROR_THRESHOLD=0.02   # 2% de errores
LATENCY_THRESHOLD=0.5  # 500ms p99
WAIT_SECONDS=120

echo "Esperando ${WAIT_SECONDS}s para acumular métricas con canary weight=${CANARY_WEIGHT}%..."
sleep $WAIT_SECONDS

# Consulta error rate
ERROR_RATE=$(curl -sf "${PROMETHEUS_URL}/api/v1/query" \
  --data-urlencode 'query=sum(rate(http_requests_total{job="go-service-canary",status=~"5.."}[5m])) / sum(rate(http_requests_total{job="go-service-canary"}[5m]))' \
  | jq -r '.data.result[0].value[1] // "0"')

# Consulta p99 latency
LATENCY=$(curl -sf "${PROMETHEUS_URL}/api/v1/query" \
  --data-urlencode 'query=histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{job="go-service-canary"}[5m])) by (le))' \
  | jq -r '.data.result[0].value[1] // "0"')

echo "Error rate: ${ERROR_RATE} (umbral: ${ERROR_THRESHOLD})"
echo "P99 latency: ${LATENCY}s (umbral: ${LATENCY_THRESHOLD}s)"

# Comparamos con los umbrales usando awk
ERROR_EXCEEDED=$(awk "BEGIN {print (${ERROR_RATE} > ${ERROR_THRESHOLD}) ? 1 : 0}")
LATENCY_EXCEEDED=$(awk "BEGIN {print (${LATENCY} > ${LATENCY_THRESHOLD}) ? 1 : 0}")

if [ "$ERROR_EXCEEDED" = "1" ] || [ "$LATENCY_EXCEEDED" = "1" ]; then
  echo "CRÍTICO: las métricas han superado los umbrales permitidos. Iniciando rollback..."
  kubectl annotate ingress go-service-canary \
    nginx.ingress.kubernetes.io/canary-weight=0 \
    --overwrite -n production
  kubectl scale deployment/go-service-canary --replicas=0 -n production
  echo "Rollback completado. Despliegue canary interrumpido."
  exit 1
fi

echo "Métricas dentro de los límites. Continuando el despliegue."
exit 0

El script se invoca desde el pipeline en cada etapa antes de incrementar el tráfico. Al salir con código 1, GitHub Actions detiene el workflow y los pasos siguientes no se ejecutan: el rollback ya se ha producido.

Caso real: despliegue canary de un servicio Go con cero downtime

Veamos un escenario típico: un servicio Go de procesamiento de pagos con ~500 RPS en producción. El equipo lanza una nueva versión con optimización de consultas SQL. Las consecuencias de un error son críticas: incluso un 1% de respuestas 5xx en un servicio de pagos es inaceptable.

  1. Construcción de la imagen: GitHub Actions construye la imagen Docker del servicio Go y la sube al registry con el tag del SHA del commit.
  2. Despliegue canary al 5%: Se lanza 1 nuevo pod; el Ingress se configura con el 5% del tráfico. Se espera 2 minutos.
  3. Verificación de métricas: Prometheus registra un error rate del 0,1% (normal) y una latencia p99 de 120ms (normal ≤500ms). El pipeline continúa.
  4. Incremento al 20%: Se añaden réplicas y se cambia el peso. Nueva verificación tras 3 minutos: todo dentro de los límites.
  5. Transición completa: Se actualiza el Deployment stable con la nueva imagen mediante rolling update, se elimina el peso canary y se escala el canary a 0. Downtime: 0 segundos.

Todo el proceso tomó unos 15 minutos, de forma automática y sin intervención manual. El enfoque GitOps (manifiestos en Git, cambios solo a través del pipeline) garantiza la auditoría de cada modificación.

Errores habituales y cómo evitarlos

  • Ventana de observación demasiado corta: 30 segundos no son suficientes para obtener métricas estadísticamente significativas con bajo RPS. El mínimo recomendado es de 2 a 5 minutos según el tráfico.
  • Ignorar el calentamiento de JVM/runtime de Go: Los primeros segundos tras el inicio del pod, la latencia será más alta de lo normal. Usa readinessProbe y startupProbe para que el tráfico solo llegue al pod cuando esté listo.
  • Canary sin aislamiento de sticky sessions: Si los usuarios deben permanecer en la misma versión (por ejemplo, en un test A/B), usa nginx.ingress.kubernetes.io/canary-by-cookie o canary-by-header.
  • Ausencia de alertas para canary bloqueado: Si el pipeline falla y el Ingress canary queda con un peso del 20%, se trata de una situación anómala. Añade una regla de Alertmanager para canary Ingress activo durante un tiempo prolongado.
  • Service único para ambos Deployments: Si se requiere un enrutamiento por pesos preciso, usa Services separados para stable y canary, y gestiona la distribución del tráfico únicamente a través del Ingress/Gateway API.
  • Sin límites de recursos en el canary: Un pod canary sin limits puede consumir los recursos de los pods vecinos. Especifica siempre requests y limits.

Conclusión y checklist

El despliegue canary en Kubernetes es un enfoque maduro para el despliegue seguro que, correctamente configurado, elimina por completo el control manual. La combinación Kubernetes + NGINX Ingress + Prometheus + CI/CD ofrece un ciclo completo: despliegue → observación → decisión automática (continuar o revertir).

Un despliegue seguro no es el heroísmo del ingeniero de guardia a las 3 de la madrugada, sino una automatización bien configurada que resuelve el problema antes de que los usuarios lo perciban.

Checklist para implementar el despliegue canary:

  • Deployments separados creados para las versiones stable y canary
  • Ingress configurado con las anotaciones canary-weight
  • ServiceMonitor o anotaciones de Pod para la recopilación de métricas de Prometheus
  • Umbrales definidos para error rate y latencia (por ejemplo, <2% de errores, p99 <500ms)
  • Script de verificación de métricas integrado en el pipeline de CI/CD
  • Rollback implementado como paso explícito del pipeline con código de salida 1
  • Readiness y Liveness probes configurados en el pod canary
  • Alerta configurada para canary Ingress "bloqueado"
  • Manifiestos almacenados en Git (enfoque GitOps)
  • Prueba de ejecución realizada con error intencionado para verificar el rollback

Tecnologías

Etiquetas

Ruslan Ismailov

Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →