Despliegue Canary en Kubernetes con rollback automático mediante CI/CD y métricas de Prometheus
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.
- 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.
- Despliegue canary al 5%: Se lanza 1 nuevo pod; el Ingress se configura con el 5% del tráfico. Se espera 2 minutos.
- 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.
- 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.
- 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-cookieocanary-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
limitspuede consumir los recursos de los pods vecinos. Especifica siemprerequestsylimits.
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í →