Módulo 4: Environments, Secrets & Rollback
5. Despliegues blue-green
Qué cubre esta cápsula
En la cápsula anterior construiste un pipeline multi-stage que va dev → staging → producción. Funciona, pero el deploy a producción todavía tiene una limitación: cuando reemplaza la versión vieja con la nueva, hay un momento donde los usuarios pueden ver errores (durante el reinicio del container) o experimentar comportamiento mixto (si hay múltiples replicas y solo algunas se actualizaron).
Esta cápsula introduce blue-green deployment — el patrón que elimina downtime durante deploys y permite rollback en segundos. Vas a entender el concepto, ver cómo se implementa en plataformas managed (Railway, Render), y aprender a implementar una versión simplificada con dos services y switch de tráfico. No vas a implementar blue-green completo con load balancer (eso requiere Kubernetes o setup más complejo), pero vas a entender el patrón lo suficiente para reconocerlo en producción y aplicarlo cuando escales.
Al terminar, podrás:
- Explicar el patrón blue-green: dos environments idénticos, switch de tráfico, rollback instantáneo
- Diferenciar blue-green del deployment rolling (default de la mayoría de PaaS)
- Identificar cuándo blue-green es la opción correcta vs cuándo no lo es
- Implementar una versión simplificada con dos Railway services + DNS switch
- Calcular el costo extra de blue-green (mantener dos environments idénticos)
- Distinguir blue-green de canary releases (siguiente cápsula)
El problema: deploys con downtime o errores intermitentes
Tu pipeline del módulo 3/4 hace deploys "rolling" (el comportamiento default):
Estado inicial:
┌──────────┐
│ App v1 │ ← todos los usuarios apuntando acá
│ replica1 │
└──────────┘
Deploy v2 empieza:
┌──────────┐ ┌──────────┐
│ App v1 │ │ App v2 │ ← Railway crea nuevo container
│ replica1 │ │ replica1 │
└──────────┘ └──────────┘
Health check de v2 OK:
┌──────────┐
│ App v2 │ ← Railway elimina v1, traffic apunta a v2
│ replica1 │
└──────────┘
Problemas:
-
Ventana de inestabilidad: durante el switch, algunas requests pueden ir a v1, otras a v2. Si hay state compartido (DB sessions, caches), comportamiento inconsistente.
-
Rollback lento: si v2 falla en producción 10 min después del deploy, hacer rollback requiere "deployar v1 de vuelta" — otro deploy completo (5+ min). Mientras tanto, prod rota.
-
Migrations destructivas peligrosas: si v1 todavía está corriendo cuando v2 corre una migration que cambia schema, v1 puede crashear.
Blue-green resuelve esto con un patrón diferente:
Estado inicial:
┌──────────┐ ┌──────────┐
│ BLUE │← traffic │ GREEN │ ← inactivo
│ App v1 │ │ (empty) │
└──────────┘ └──────────┘
Deploy v2:
┌──────────┐ ┌──────────┐
│ BLUE │← traffic │ GREEN │ ← deploy v2 acá
│ App v1 │ │ App v2 │
└──────────┘ └──────────┘
health check OK?
Switch:
┌──────────┐ ┌──────────┐
│ BLUE │ │ GREEN │← traffic
│ App v1 │ ← inactivo │ App v2 │
└──────────┘ └──────────┘
Si v2 falla 10 min después, rollback:
┌──────────┐ ┌──────────┐
│ BLUE │← traffic │ GREEN │
│ App v1 │ ← reactivado │ App v2 │ ← desactivado
└──────────┘ └──────────┘
(5 segundos)
Cero downtime durante el switch. Rollback en segundos cambiando el target del traffic.
El modelo mental: dos pistas paralelas
Piensa tu aplicación como un sistema con dos pistas paralelas (BLUE y GREEN). Solo una recibe usuarios a la vez:
Internet / DNS
│
▼
┌────────────────┐
│ Load Balancer │
│ (o DNS switch)│
└────────┬───────┘
│
¿BLUE o GREEN?
/ \
▼ ▼
┌─────────┐ ┌──────────┐
│ BLUE │ │ GREEN │
│ (v1.0) │ │ (v1.1) │
│ active │ │ standby │
└─────────┘ └──────────┘
Características:
- En cualquier momento, una pista recibe tráfico
- La otra pista existe pero está "esperando"
- Deploy = actualizar la pista que NO recibe tráfico
- Switch = cambiar el target del Load Balancer/DNS
- Rollback = switch de vuelta (segundos)
Después del switch exitoso, la pista vieja queda como "previous versión" — siempre lista para rollback si hace falta.
Variantes del patrón
Variante 1: dos services en la misma plataforma
Setup en Railway/Render:
Project: myapp
├── Service: myapp-blue
│ URL: blue.myapp.com (privado)
│ Imagen: ghcr.io/myapp:v1.0
├── Service: myapp-green
│ URL: green.myapp.com (privado)
│ Imagen: ghcr.io/myapp:v1.1
└── DNS / Edge:
myapp.com → CNAME → blue.myapp.com (actualmente)
Deploy:
- Push deploya a
myapp-green(la inactiva) - Health check verifica green OK
- Update DNS:
myapp.com→green.myapp.com - Traffic empieza a fluir a green
- blue queda como "previous" para rollback
Costo: mantener dos services activos = 2× el costo de runtime. Vale para apps con SLA crítico.
Variante 2: blue-green con feature flags
En lugar de switch de DNS, usar feature flags para que tu app sirva código viejo o nuevo según un flag:
# app/main.py
@app.get("/users")
def list_users():
if feature_flag.is_enabled("new_user_query_v2"):
return new_query_implementation()
else:
return old_query_implementation()
Deploy:
- Deploy v2 (con ambas implementaciones) — flag default a "false"
- Verificar deploy OK con flag off (corre el código viejo)
- Enable flag para 1% de usuarios (canary, siguiente cápsula)
- Si OK, enable 100%
- Si rollback: disable flag (instantáneo)
Trade-off: complejidad del código aumenta (dos implementaciones conviviendo), pero rollback es instantáneo sin redeploy.
Variante 3: Blue-green real con load balancer (avanzado)
ALB / nginx / HAProxy
│
▼
Pool A (BLUE) ─── ec2-1, ec2-2, ec2-3
OR
Pool B (GREEN) ── ec2-4, ec2-5, ec2-6
El load balancer cambia el upstream pool. Tráfico se redirige sin cambiar DNS (más rápido que DNS porque DNS tiene TTL).
Cuándo: apps con tráfico alto donde DNS TTL de minutos es inaceptable.
Implementación práctica para Railway
Versión simplificada de blue-green en Railway (sin load balancer custom):
Setup
Crear dos services en Railway:
myapp-blue: tu service actual (production)myapp-green: nuevo service idéntico (vacío inicialmente)
Configurar custom domain con switch capability:
- En Railway tu actual production tiene
myapp.com - Agregar
myapp-green.comapuntando al service green - Tu DNS provider (Cloudflare, Route53) maneja el switch
Workflow
deploy-production-blue-green:
name: Blue-green deploy to production
runs-on: ubuntu-latest
needs: deploy-staging
environment: production
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Determine target color
id: target
run: |
# Verificar cuál service está sirviendo tráfico actualmente
ACTIVE=$(curl -s https://myapp.com/_health-color | jq -r .color)
if [ "$ACTIVE" = "blue" ]; then
echo "target=green" >> $GITHUB_OUTPUT
echo "target_service=${{ secrets.RAILWAY_SERVICE_GREEN }}" >> $GITHUB_OUTPUT
else
echo "target=blue" >> $GITHUB_OUTPUT
echo "target_service=${{ secrets.RAILWAY_SERVICE_BLUE }}" >> $GITHUB_OUTPUT
fi
echo "Target color: $(cat $GITHUB_OUTPUT | grep target=)"
- name: Deploy to standby
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
run: |
npm install -g @railway/cli
railway redeploy --service ${{ steps.target.outputs.target_service }}
- name: Health check standby
run: |
# Health check al endpoint privado de la pista standby
STANDBY_URL="${{ steps.target.outputs.target }}.myapp.com"
for i in $(seq 1 24); do
CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://$STANDBY_URL/health" || echo "000")
[ "$CODE" = "200" ] && break
sleep 5
done
[ "$CODE" = "200" ] || (echo "❌ Standby unhealthy" && exit 1)
- name: Switch traffic via DNS
env:
CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
run: |
# Actualizar el CNAME de myapp.com al service target
./scripts/cloudflare-switch.sh \
--domain myapp.com \
--target ${{ steps.target.outputs.target }}.myapp.com
- name: Wait for DNS propagation
run: sleep 60 # esperar TTL bajo
- name: Verify production traffic
run: |
# Verificar que prod ahora apunta al nuevo color
COLOR=$(curl -s https://myapp.com/_health-color | jq -r .color)
if [ "$COLOR" != "${{ steps.target.outputs.target }}" ]; then
echo "❌ Switch failed — still serving from old color"
exit 1
fi
echo "✅ Switched to ${{ steps.target.outputs.target }}"
Endpoint /_health-color
Tu app necesita exponer qué "color" es:
import os
@app.get("/_health-color")
def health_color():
return {
"color": os.environ.get("DEPLOYMENT_COLOR", "unknown"),
"version": os.environ.get("DEPLOYMENT_VERSION", "unknown"),
}
Cada service tiene su env var:
myapp-blue:DEPLOYMENT_COLOR=bluemyapp-green:DEPLOYMENT_COLOR=green
Así tu workflow sabe cuál está sirviendo tráfico.
Rollback con blue-green
El gran beneficio: rollback instantáneo.
Escenario: acabas de switchear a green. Después de 10 min, monitoring muestra error rate alto en green. Rollback:
rollback-blue-green:
name: Emergency rollback
runs-on: ubuntu-latest
steps:
- name: Determine current and previous colors
id: colors
run: |
CURRENT=$(curl -s https://myapp.com/_health-color | jq -r .color)
if [ "$CURRENT" = "blue" ]; then
PREVIOUS=green
else
PREVIOUS=blue
fi
echo "previous=$PREVIOUS" >> $GITHUB_OUTPUT
- name: Switch DNS back to previous
env:
CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
run: |
./scripts/cloudflare-switch.sh \
--domain myapp.com \
--target ${{ steps.colors.outputs.previous }}.myapp.com
- name: Verify rollback
run: |
sleep 60 # DNS propagation
COLOR=$(curl -s https://myapp.com/_health-color | jq -r .color)
if [ "$COLOR" != "${{ steps.colors.outputs.previous }}" ]; then
exit 1
fi
Total tiempo de rollback: ~90 segundos (60s DNS TTL + 30s de switch).
Esto es 10× más rápido que rollback con deploy reverso.
El gotcha: database migrations en blue-green
Blue-green funciona bien para código stateless. Pero la base de datos es shared state:
BLUE (v1):
Espera columna `users.email`
GREEN (v2):
Migration ya aplicada
Espera columna `users.contact_email` (renamed)
DB (shared):
Migration aplicada → tiene `contact_email`
Columna `email` eliminada
→ BLUE crashea (busca columna que no existe)
Estrategia: expand-contract pattern (introducido en cápsula 04 del módulo 3):
Deploy 1: Migration aditiva → DB tiene email Y contact_email
Código v1 escribe a email
Code v2 escribe a ambas, lee de contact_email si existe
Deploy 2: Código v3 — solo usa contact_email
DB: ambas columnas todavía existen
Deploy 3: Migration destructiva → drop email
Solo después de verificar que nadie la usa
Tres deploys para hacer un rename "destructivo". Tedioso pero safe en blue-green.
Regla: durante blue-green, las migrations deben ser backward-compatible con la versión anterior. Si v2 hace cambios destructivos al schema, v1 va a romper durante el período donde ambas conviven.
¿Cuándo NO usar blue-green?
Blue-green tiene costos:
Costo 1: 2× el costo de runtime
Mantienes dos services idénticos corriendo. Si tu app cuesta $50/mes, ahora cuesta $100/mes.
Cuándo vale: apps con SLA crítico (B2B donde un minuto de downtime cuesta más que el extra mensual).
Cuándo NO vale: proyectos personales, MVPs, apps internas con tolerancia a downtime.
Costo 2: complejidad del schema
Cada migration destructiva requiere expand-contract = más PRs, más cuidado.
Cuándo vale: apps donde el schema cambia raramente.
Cuándo NO vale: apps en early development con schema cambiando constantemente.
Costo 3: DNS TTL
DNS switch tarda 30-300 segundos según TTL. Si necesitas switch instantáneo, blue-green con DNS no es suficiente — necesitas load balancer real.
Cuándo vale: apps que toleran 1-2 min de propagación.
Cuándo NO vale: apps con tráfico financiero donde cada segundo importa.
Alternativas
- Rolling deploy (default): suficiente para 80% de los casos
- Canary (siguiente cápsula): mejor cuando quieres validar gradualmente
- Feature flags: zero-downtime sin duplicar infrastructure
- Blue-green real con load balancer: máximo control, máxima complejidad
Trampas comunes
1. Olvidar que la DB es compartida
Blue (v1) → DB
Green (v2) → DB (misma)
Si v2 hace un cambio que rompe v1 en la DB, blue-green falla.
Cómo manejar: expand-contract para migrations destructivas. Período de overlap donde ambas versiones funcionan.
2. DNS TTL alto
TTL: 3600 segundos (1 hora)
Switch toma hasta 1 hora en propagar globalmente. Rollback no es "instantáneo".
Cómo manejar: TTL de 60-300s para tu dominio principal en producción.
3. Health check en standby no representativo
Health check en green → /health → 200 OK
Switch traffic → real users → 500 errors
/health puede no probar todos los paths críticos. Real traffic descubre problemas que health checks no.
Cómo manejar:
/healthdebe verificar dependencias críticas (DB, cache)- Agregar smoke tests más exhaustivos antes del switch
- Después del switch, monitorear error rate los primeros 5-10 min
4. State en memoria (cache, sessions)
Si tu app guarda state en memoria (en el container), switchear desactiva ese state:
Blue: tiene cache con datos de 30 min
Green: cache vacío
Switch → primer minute green tiene cache miss masivo → slow responses
Cómo manejar:
- State en Redis (compartido)
- Sessions en DB
- Warmup script post-switch que precarga cache crítica
5. Hardcodear el color "actual"
- run: railway redeploy --service blue # ❌ siempre blue
Workflow asume blue es siempre el target. Después de 2 deploys, blue ↔ green dejan de tener sentido.
Cómo manejar: detectar el color actual via API/endpoint, deployar al opuesto:
CURRENT=$(curl -s /_health-color | jq -r .color)
TARGET=$([ "$CURRENT" = "blue" ] && echo "green" || echo "blue")
railway redeploy --service "$TARGET"
Caso desarrollado: blue-green ahorra una noche
Sin blue-green:
Lunes 23:00 — deployas v2 a prod (rolling).
23:05 — v2 corre. Pero tiene un bug sutil: bajo carga, hay deadlock cada 100 requests.
23:30 — Empiezas a recibir alerts de usuarios.
23:45 — Investigas. Es deadlock. Necesitas rollback.
23:50 — git revert HEAD && git push
23:55 — CI corre (5 min)
00:00 — Build corre (2 min)
00:02 — Deploy corre (3 min)
00:05 — Health check OK
00:10 — Monitoreas, parece estable
01:00 — Te vas a dormir, después de 2 horas de incident.
Con blue-green:
Lunes 23:00 — deployas v2 a green (auto).
23:01 — health check green OK
23:01 — DNS switch a green
23:05 — v2 sirviendo tráfico. Bug sutil. Deadlock cada 100 requests.
23:30 — Alerts empiezan.
23:45 — Investigas, identificas deadlock. Decides rollback.
23:46 — Ejecutas el workflow rollback-blue-green
23:46 — DNS switch a blue (v1)
23:47 — DNS propaga (TTL 60s)
23:48 — Prod en v1 estable. 3 minutos total.
Diferencia: 2 horas vs 3 minutos. Y tú durmiendo bien.
Auto-verificación
1. ¿Cuál es la diferencia entre blue-green y rolling deploy?
Rolling deploy (default en la mayoría de PaaS):
- Replicas se actualizan una por una: replica 1 con v1 + replica 2 con v1 → replica 1 con v2 + replica 2 con v1 → replica 1 con v2 + replica 2 con v2.
- Durante el deploy, requests pueden ir a v1 o v2 según qué replica responde.
- Trade-off: sin doble infrastructure cost, pero período de inconsistencia (1-5 min).
- Rollback: redeploy de versión vieja (otro deploy completo, 5+ min).
Blue-green:
- Dos pools separados (blue y green). Solo una recibe tráfico en cualquier momento.
- Deploy se hace en la inactiva. Switch instantáneo cuando lista.
- Trade-off: doble cost de runtime, pero zero downtime y rollback en segundos.
- Rollback: switch DNS a la pista anterior. 30-90 segundos.
Cuándo usar cada uno:
- Rolling: 80% de las apps. Suficiente para la mayoría.
- Blue-green: SLA crítico, sin tolerancia a inconsistencia durante deploys, equipos que quieren rollback super-rápido.
Híbridos: algunos PaaS modernas tienen "atomic deployments" que son rolling pero con health check estricto antes del switch (parcialmente blue-green-like). Railway/Render usan este patrón.
2. Tu app usa sesiones en memoria. ¿Por qué blue-green es problemático?
El problema:
Cuando switcheas de blue a green, los containers que estaban manteniendo sesiones de usuarios se eliminan o quedan inactivos. Las sesiones se pierden.
Ejemplos de impacto:
- Usuario está completando un form multi-step (sesión guarda los datos parciales)
- Usuario está autenticado (sesión guarda el JWT pero también state como "última visita")
- Usuario está en un carrito de compra que vive en memoria
Después del switch:
- "Tu sesión expiró, por favor inicia sesión nuevamente"
- Formulario reinicia desde cero
- Carrito vacío
Soluciones:
1. Sesiones en storage externo (la solución correcta):
# En lugar de in-memory:
sessions = {} # ❌ pierde con restart
# Redis:
sessions = redis.Redis(host="cache.example.com") # ✅ persiste through restart
Cuando switcheas, sesiones siguen vivas en Redis. Ambos blue y green las ven.
2. Sticky sessions con database:
Cada request lleva un session_id. Backend lee/escribe estado en DB. Switch no afecta porque DB es shared.
3. Sesiones en cookies (stateless):
JWT con todo el state necesario codificado. Backend valida la firma pero no mantiene estado.
Principio del 12-factor: procesos sin estado. State vive en backing services (DB, Redis, S3). Esto hace blue-green (y rolling, y canary) trivialmente seguros.
Resumen y siguiente paso
- Blue-green = dos environments idénticos, solo uno activo a la vez, switch via DNS o load balancer
- Beneficios: zero-downtime durante deploys, rollback en segundos
- Costos: 2× runtime cost, complejidad de migrations (expand-contract)
- DB es shared — migrations deben ser backward-compatible con la versión anterior
- State en memoria pierde durante switch — usar storage externo (Redis, DB)
- Para 80% de apps, rolling deploy es suficiente. Blue-green vale cuando SLA es crítico
Puente al próximo paso: Blue-green es "all-or-nothing": todo el tráfico va a blue o a green. ¿Qué pasa si quieres probar v2 con solo 10% de usuarios primero, ver métricas, y después escalar a 100%? Eso es canary deployment, el tema de la cápsula 06. Permite reducir blast radius de un mal deploy a un subset de usuarios.
Recursos
- Martin Fowler — BlueGreenDeployment — la referencia canónica del patrón.
- Atlassian — Blue Green Deployments — guía práctica.
- Google Cloud — Implementing Blue/Green — implementaciones con GCP.
- LaunchDarkly — Feature flags vs Blue-Green — comparación.
- Heroku Preboot — versión simplificada que Heroku ofrece.
Cápsula 05 de 08 — Módulo 4 — CI/CD for Python Backend Guide