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:

  1. Ventana de inestabilidad: durante el switch, algunas requests pueden ir a v1, otras a v2. Si hay state compartido (DB sessions, caches), comportamiento inconsistente.

  2. 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.

  3. 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:

  1. Push deploya a myapp-green (la inactiva)
  2. Health check verifica green OK
  3. Update DNS: myapp.comgreen.myapp.com
  4. Traffic empieza a fluir a green
  5. 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:

  1. Deploy v2 (con ambas implementaciones) — flag default a "false"
  2. Verificar deploy OK con flag off (corre el código viejo)
  3. Enable flag para 1% de usuarios (canary, siguiente cápsula)
  4. Si OK, enable 100%
  5. 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.com apuntando 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=blue
  • myapp-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:

  • /health debe 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

  1. Martin Fowler — BlueGreenDeployment — la referencia canónica del patrón.
  2. Atlassian — Blue Green Deployments — guía práctica.
  3. Google Cloud — Implementing Blue/Green — implementaciones con GCP.
  4. LaunchDarkly — Feature flags vs Blue-Green — comparación.
  5. Heroku Preboot — versión simplificada que Heroku ofrece.

Cápsula 05 de 08 — Módulo 4 — CI/CD for Python Backend Guide