Las cuatro lecciones anteriores han montado las piezas por separado: dónde vive el código, cómo se construye y se prueba, cómo se despliega sin cortar el servicio, y cómo se encadena todo. Esta lección no añade servicios nuevos. Coge un cambio real de Luis y lo sigue desde su portátil hasta producción, paso a paso, viendo en cada punto qué se comprueba y qué pasa exactamente si falla.

El cambio elegido no es trivial a propósito. Sara ha pedido añadir una franja horaria de entrega al pedido, y eso toca cuatro cosas a la vez: el código de la tienda, un consumidor de cola-mercadofresco-pedidos, el esquema de Aurora y el contrato del evento PedidoConfirmado. Es exactamente el tipo de cambio que en el módulo 7 provocó un incidente —un productor desplegado antes que su consumidor— y que aquí vamos a hacer sin que nadie escriba un comando ni cruce los dedos.

Aviso de coste. El recorrido completo de este cambio —cuatro ejecuciones del pipeline entre desarrollo, preproducción y producción, más las construcciones de las PR— cuesta unos 0,60 USD. AppConfig, que aparece al final, cobra 0,0000002 USD por petición de configuración con caché de 60 s: para MercadoFresco, céntimos al mes. Datos ficticios.

Contenido

  1. El cambio: franja horaria de entrega
  2. El recorrido completo, de un vistazo
  3. Rama, commits y pull request
  4. Construcción, pruebas y artefacto
  5. Compatibilidad de esquema: expansión y contracción en Aurora
  6. Compatibilidad de contrato: versionar PedidoConfirmado
  7. El orden correcto entre productor y consumidor
  8. Despliegue en desarrollo, humo y aprobación de Marta
  9. Canario en producción, vigilancia y promoción
  10. Configuración y secretos por entorno
  11. La pirámide de pruebas aplicada
  12. Métricas DORA: MercadoFresco antes y después
  13. Runbook de reversión y el fallo que se detecta tarde
  14. Despliegues pequeños y banderas de funcionalidad
  15. El viernes por la tarde como decisión de negocio
  16. Cierre del módulo y coste
  17. Errores comunes y consejos
  18. Ejercicios
  19. Conclusión

El cambio: franja horaria de entrega

Sara lo justifica con un dato: el 31 % de las incidencias de reparto son por ausencia del cliente. Cada una cuesta un segundo intento, una llamada y, en producto fresco, a veces la mercancía. La petición es añadir tres franjas —manana, tarde, 24h— al proceso de compra.

Lo que parece un campo de formulario toca cinco piezas:

Pieza Qué cambia Riesgo
Tienda web (asg-mercadofresco-tienda) Selector en el proceso de compra Bajo
API de pedidos Acepta y persiste franja_entrega Medio
Esquema de Aurora Columna nueva en pedidos Alto: no revierte con el código
Evento PedidoConfirmado Atributo nuevo en el detail Alto: hay consumidores ajenos
Consumidor de almacén Lee la franja para ordenar las rutas Medio

Los dos riesgos altos son el corazón de la lección, y comparten una propiedad: son las dos cosas que la reversión automática de CodeDeploy no puede deshacer. Todo lo demás vuelve atrás en noventa segundos.

El recorrido completo, de un vistazo

flowchart TB
    A[Luis: rama funcionalidad/franja-horaria] --> B[Push: pipeline sobre PR<br/>construye y prueba]
    B -->|Rojo| B1[La PR no se puede fusionar]
    B -->|Verde| C[PR 47: revision de Marta<br/>1 aprobacion obligatoria]
    C --> D[Squash a desarrollo]
    D --> E[Construccion: artefacto<br/>1.6.0-a3f9c21 inmutable]
    E --> F[Calidad: estatico,<br/>contrato, secretos]
    F --> G[Migracion EXPANDIR<br/>columna NULLABLE]
    G --> H[Desplegar consumidor<br/>PRIMERO]
    H --> I[Desplegar tienda en dev]
    I --> J[Humo dev: flujo de compra]
    J --> K[Preproduccion + humo]
    K --> L[APROBACION MARTA<br/>version, diff, migracion]
    L --> M[Produccion: blue/green<br/>+ canario en cobros]
    M --> N[Puerta de calidad<br/>15 min de metricas]
    N -->|OK| O[Promocion: etiqueta v1.6.0]
    N -->|Degradado| P[REVERSION automatica<br/>90 s]
    O --> Q[Semanas despues:<br/>CONTRAER]

Rama, commits y pull request

Luis parte de desarrollo, siguiendo el modelo de 08-01, y se impone el límite de tres días:

git checkout desarrollo && git pull
git checkout -b funcionalidad/franja-horaria

Los commits, con el formato convencional que el pipeline leerá después:

feat(pedidos): anadir franja_entrega al modelo y a la API
feat(eventos): incluir franja_entrega en PedidoConfirmado como version 2
feat(almacen): ordenar rutas de reparto por franja
test(pedidos): cubrir franjas validas, invalida y ausente
docs(catalogo-eventos): documentar PedidoConfirmado v2

El primer feat determina que la versión suba a 1.6.0 —MINOR, funcionalidad compatible—. Si alguno llevara BREAKING CHANGE: en el cuerpo, sería 2.0.0, y eso es justamente lo que vamos a evitar.

Al empujar la rama, el disparador de pull request de 08-04 arranca el pipeline en modo reducido: construcción, pruebas y calidad, sin etapas de despliegue. El resultado se publica en la PR como comprobación de estado, y la PR no se puede fusionar si está en rojo. Aquí se cierra por fin la protección de main que quedó abierta en 08-01: no basta con que alguien apruebe, la construcción tiene que estar en verde.

La PR #47 lleva la plantilla de 08-01, con una sección que en este cambio es la más importante:

## Riesgos
- Esquema Aurora: columna NULLABLE, sin DEFAULT, sin indice. Fase EXPANDIR.
  NO revierte con el codigo. Contraccion prevista para dentro de 3 semanas.
- Evento PedidoConfirmado pasa a version 2: atributo OPCIONAL anadido.
  Consumidores v1 lo ignoran. Verificado con pruebas de contrato.
- Orden de despliegue: consumidor de almacen ANTES que la tienda.

Marta revisa y aprueba; Luis fusiona con squash, y el commit resultante entra en desarrollo.

Construcción, pruebas y artefacto

Al fusionar, el pipeline arranca completo. La etapa de construcción hace lo de 08-02 y produce mercadofresco-tienda-1.6.0-a3f9c21.zip, con el commit en los metadatos.

Esto es lo único que se construye en todo el recorrido. El mismo objeto de S3 se desplegará en desarrollo, en preproducción y en producción. Si el pipeline reconstruyera antes de producción, la aprobación de Marta dejaría de significar nada, como vimos en el ejercicio 1 de 08-04.

Comprobación Bloquea Qué pasa si falla
ruff y formato Falla en 2 s; Luis corrige y vuelve a empujar
git-secrets Falla y hay que rotar si el secreto era real
Pruebas unitarias (218) La PR no se fusiona
Cobertura ≥ 75 % Escribir las pruebas que faltan
pip-audit Sí, con aviso Actualizar la dependencia o documentar
Pruebas de contrato La clave de este cambio; ver más abajo
bandit No, informe Se revisa los lunes

Compatibilidad de esquema: expansión y contracción en Aurora

Aplicando la regla de 08-03, el cambio de esquema no va en el mismo paso que el código y se parte en tres fases separadas en el tiempo.

Fase 1, EXPANDIR, en una etapa propia del pipeline antes de cualquier despliegue de aplicación:

-- migraciones/2026-08-02-001-expandir-franja-entrega.sql
-- Compatible con la version 1.5.2, que no conoce esta columna.
ALTER TABLE pedidos ADD COLUMN franja_entrega VARCHAR(10) NULL;
-- Sin NOT NULL: la 1.5.2 inserta sin la columna y no falla.
-- Sin DEFAULT: no reescribe 4,2 millones de filas ni bloquea la tabla.
-- Sin indice: se anade en la fase de contraccion, con CONCURRENTLY.

Los tres comentarios son el ejercicio entero. Con NOT NULL, la versión anterior fallaría en cada inserción durante el despliegue gradual. Con DEFAULT, en PostgreSQL 11 y posteriores no se reescribe la tabla, pero en motores anteriores sí, y sobre 4,2 millones de filas es un bloqueo de minutos. Y el índice se pospone porque CREATE INDEX bloquea escrituras: se hará con CONCURRENTLY y fuera del despliegue.

Fase 2, MIGRAR, es el código 1.6.0: escribe franja_entrega cuando viene y tolera NULL cuando no.

Fase 3, CONTRAER, tres semanas después y como cambio propio, cuando ninguna instancia sirve ya la 1.5.2 y se ha comprobado que todos los pedidos nuevos traen franja:

CREATE INDEX CONCURRENTLY idx_pedidos_franja ON pedidos(franja_entrega);
UPDATE pedidos SET franja_entrega = '24h' WHERE franja_entrega IS NULL;
ALTER TABLE pedidos ALTER COLUMN franja_entrega SET NOT NULL;

El control automático que impide saltarse esto es el análisis de migraciones en CodeBuild que diseñamos en el ejercicio 3 de 08-03: falla la construcción si detecta DROP COLUMN, RENAME COLUMN, SET NOT NULL o DROP TABLE en un fichero de migración sin la etiqueta -- CONTRACCION-APROBADA. La fase 3 la lleva y la fase 1 no la necesita.

Compatibilidad de contrato: versionar PedidoConfirmado

El otro riesgo alto. PedidoConfirmado lo consumen tres destinos de bus-mercadofresco: la cola del almacén, la de analítica y la API del ERP del socio. Los dos últimos no los controla Luis, y ahí está el problema: publicar un evento distinto puede romper a alguien que ni sabes que está escuchando.

Cambio ¿Compatible? Qué hacer
Añadir un campo opcional Subir la versión menor; adelante
Añadir un campo obligatorio No Tratarlo como opcional con valor por defecto
Quitar un campo No Deprecar, avisar, esperar, quitar
Renombrar un campo No Añadir el nuevo, publicar ambos, quitar el viejo
Cambiar el tipo de un campo No Campo nuevo con nombre distinto
Restringir valores permitidos No Ampliar, nunca restringir

Añadir franja_entrega como campo opcional es el caso fácil, y el evento sube a versión 2:

{
  "version": "2",
  "source": "mercadofresco.tienda",
  "detail-type": "PedidoConfirmado",
  "detail": {
    "id_pedido": "PED-2026-00841",
    "id_cliente": "CLI-4471",
    "importe_eur": 48.20,
    "lineas": [{ "sku": "FRUT-0012", "uds": 3 }],
    "franja_entrega": "tarde",
    "confirmado_en": "2026-08-02T18:14:22Z"
  }
}

Un consumidor v1 recibe un objeto con un campo que no conoce y lo ignora, siempre que esté escrito con la regla de oro: sé estricto con lo que publicas y tolerante con lo que recibes. Un consumidor que valide el esquema con additionalProperties: false se rompería, y por eso la prueba de contrato es bloqueante:

# tests/contrato/test_pedido_confirmado.py
import pytest
from app.eventos import construir_evento_pedido_confirmado
from consumidores.almacen_v1 import procesar as procesar_v1   # el consumidor ANTIGUO
from consumidores.almacen_v2 import procesar as procesar_v2

PEDIDO = {"id_pedido": "PED-2026-00841", "id_cliente": "CLI-4471",
          "importe_eur": 48.20, "lineas": [{"sku": "FRUT-0012", "uds": 3}],
          "franja_entrega": "tarde"}

def test_consumidor_ANTIGUO_no_se_rompe_con_el_evento_NUEVO():
    """Compatibilidad hacia delante: v1 debe ignorar el campo que no conoce."""
    r = procesar_v1(construir_evento_pedido_confirmado(PEDIDO))
    assert r["estado"] == "procesado"          # no lanza, no rechaza

def test_consumidor_NUEVO_tolera_el_evento_ANTIGUO():
    """Compatibilidad hacia atras: durante el despliegue habra eventos v1 en vuelo."""
    sin_franja = {k: v for k, v in PEDIDO.items() if k != "franja_entrega"}
    ev = construir_evento_pedido_confirmado(sin_franja)
    r = procesar_v2(ev)
    assert r["estado"] == "procesado"
    assert r["franja_asignada"] == "24h"        # valor por defecto explicito

def test_franja_invalida_se_rechaza_antes_de_publicar(bus_falso):
    with pytest.raises(ValueError, match="franja_entrega"):
        construir_evento_pedido_confirmado({**PEDIDO, "franja_entrega": "noche"})
    assert bus_falso.eventos_publicados == 0

Las dos pruebas centrales son la segunda y la tercera, y conviene entender por qué se necesitan las dos. La segunda comprueba compatibilidad hacia delante: el consumidor viejo con el evento nuevo, que es lo que ocurre en cuanto la tienda empieza a publicar la v2 y todavía queda algún consumidor sin actualizar. La tercera comprueba compatibilidad hacia atrás: el consumidor nuevo con el evento viejo, que es lo que ocurre con los mensajes que ya estaban en la cola cuando se desplegó el consumidor. Durante un despliegue gradual las dos situaciones existen a la vez, y una prueba sola solo cubre la mitad.

Para el ERP del socio, que no se puede probar, la salvaguarda es distinta: el archivo de EventBridge que activamos en 07-03. Si el socio se rompe, hay 90 días de eventos para reproducir.

El orden correcto entre productor y consumidor

Aquí se cierra el incidente que quedó planteado en el módulo 7. La regla es corta y vale para cualquier cambio de contrato:

Despliega siempre primero el que lee, después el que escribe.

El razonamiento es de teoría de conjuntos. Un consumidor nuevo entiende los eventos viejos y los nuevos —porque lo hemos probado—, así que desplegarlo primero es seguro: durante la ventana solo hay eventos viejos, que sabe procesar. Al revés no funciona: si el productor sale primero, hay eventos v2 circulando hacia consumidores v1 durante los minutos que dure el despliegue, y basta con que uno valide estrictamente para que aparezcan mensajes en mercadofresco-pedidos-fallidos.

En el pipeline esto es una etapa con dos acciones y runOrder distinto, que es exactamente el mecanismo de 08-04:

{
  "name": "DesplegarProduccion",
  "actions": [
    { "name": "ConsumidorAlmacen", "runOrder": 1,
      "configuration": { "ApplicationName": "app-mercadofresco-trabajadores",
                         "DeploymentGroupName": "dg-mercadofresco-trabajadores-produccion" },
      "inputArtifacts": [{ "name": "PaqueteTienda" }] },
    { "name": "TiendaWeb", "runOrder": 2,
      "configuration": { "ApplicationName": "app-mercadofresco-tienda",
                         "DeploymentGroupName": "dg-mercadofresco-tienda-produccion" },
      "inputArtifacts": [{ "name": "PaqueteTienda" }] }
  ]
}

runOrder: 1 para el consumidor y 2 para la tienda. El orden deja de depender de que alguien se acuerde: está declarado, versionado y se ejecuta igual todas las veces. Ese es, en una frase, el valor de todo el módulo 8.

Despliegue en desarrollo y pruebas de humo

En desarrollo el pipeline usa AllAtOnce porque la velocidad importa más que la disponibilidad. Las pruebas de humo de 08-04 corren contra el entorno recién desplegado y añaden dos casos de este cambio:

def test_pedido_con_franja_se_confirma_y_persiste():
    r = _crear_pedido({"franja_entrega": "tarde"})
    assert r.status_code == 201
    assert r.json()["franja_entrega"] == "tarde"
    assert r.elapsed.total_seconds() < 2.0

def test_pedido_SIN_franja_sigue_funcionando():
    """Compatibilidad hacia atras contra el entorno real, no simulado."""
    r = _crear_pedido({})
    assert r.status_code == 201
    assert r.json()["franja_entrega"] in (None, "24h")

La segunda prueba es la que de verdad importa: verifica contra un entorno real que un cliente con la app antigua en el móvil sigue pudiendo comprar. Si falla, el pipeline se detiene en desarrollo y nadie se ha enterado fuera del equipo.

La aprobación de Marta

A las 10:42 le llega la notificación a alertas-mercadofresco y a Slack:

Pipeline: pipeline-mercadofresco-tienda | Ejecucion: 7a1f2c33
Version 1.6.0-a3f9c21 | Autor: luis | PR #47
Humo preproduccion: OK (12/12) | Latencia p95 preprod: 380 ms
MIGRACION: fase EXPANDIR aplicada (columna NULLABLE, no revierte con el codigo)
EVENTO: PedidoConfirmado pasa a v2 (atributo opcional)
Ventana: OK (martes 10:42, fuera de la franja prohibida)
Diff: github.com/mercadofresco/mercadofresco-tienda/compare/v1.5.2...1.6.0-a3f9c21

Marta abre el diff, ve 214 líneas en cuatro ficheros, comprueba que la migración es de expansión y aprueba. Cuarenta segundos. No porque confíe a ciegas, sino porque el mensaje responde por adelantado a las preguntas que se haría: qué entra, quién lo hizo, si se probó, qué no revierte y si es buen momento.

Canario en producción y vigilancia

La etapa de producción hace tres despliegues coordinados:

Orden Qué Estrategia Duración
1 Consumidor de almacén En el sitio, AllAtOnce ~2 min
2 Lambda mercadofresco-cobrar-pago Canary10Percent5Minutes ~5 min
3 Tienda web Blue/green, espera de 30 min ~12 min

Durante el proceso vigilan tres alarmas conectadas a la reversión automática: mercadofresco-alb-latencia-alta, mercadofresco-pedidos-fallidos y mercadofresco-cobrar-pago-errores-canario. Y al terminar, la puerta de calidad de 08-04 observa 15 minutos las métricas de MercadoFresco/Tienda, con la ventana dimensionada para caber dentro de los 30 minutos de terminationWaitTimeInMinutes —si la puerta tardara más, el entorno azul ya estaría terminado y la reversión de 90 segundos habría dejado de existir—.

Promoción o reversión automática

Si todo va bien, el pipeline termina, CodeDeploy termina el entorno azul a los 30 minutos y una acción final etiqueta la versión:

git tag -a v1.6.0 -m "Franja horaria de entrega"
git push origin v1.6.0

Lo que se etiqueta es lo que llegó a producción y sobrevivió a la puerta de calidad, no lo que se construyó. Es una diferencia importante: la etiqueta certifica un hecho, no una intención.

Si algo falla, cada mecanismo actúa en su plazo:

Fallo Quién lo detecta Reacción Tiempo
Errores en el canario Alarma del alias CodeDeploy devuelve el peso Segundos
Latencia alta tras el cambio de tráfico mercadofresco-alb-latencia-alta Oyente vuelve al azul ~90 s
Mensajes en la DLQ mercadofresco-pedidos-fallidos Reversión + aviso ~2 min
Degradación sutil sin alarma Puerta de calidad Detiene el pipeline ~15 min
Nada de lo anterior Una persona Runbook Minutos u horas

Y la parte que ningún mecanismo revierte: la columna franja_entrega sigue ahí. No pasa nada, porque es NULLABLE y la 1.5.2 la ignora. Ese es exactamente el motivo de que la fase de expansión sea inocua: convierte un cambio irreversible en uno que no molesta.

Configuración y secretos por entorno

El artefacto es el mismo en los tres entornos, así que la diferencia entre ellos no puede estar en el paquete: está en la configuración que se lee al arrancar, con lo que montamos en 04-03.

aws ssm put-parameter --name /mercadofresco/produccion/franjas_entrega_activas \
  --value "manana,tarde,24h" --type String --overwrite \
  --profile mercadofresco-dev --region eu-west-1
Tipo de valor Dónde vive Ejemplo
Configuración no sensible Parameter Store /mercadofresco/<entorno>/ Franjas activas, umbrales
Secretos Secrets Manager, con rotación mercadofresco/produccion/rds/mfadmin
Constantes del código En el repositorio Formato de fecha, códigos de error
Valores «a mano» En ningún sitio

La última fila es la regla: si un valor difiere entre entornos, tiene que estar en Parameter Store. Un valor puesto a mano en una instancia desaparece en el siguiente despliegue blue/green —las instancias son nuevas— y reaparece como un fallo intermitente imposible de diagnosticar.

La pirámide de pruebas aplicada

Nivel Cuántas Dónde corren Duración ¿Bloquea?
Unitarias 218 CodeBuild, sin red 45 s
Integración 34 CodeBuild con moto 1 min
Contrato 12 CodeBuild 15 s
Humo 12 Contra dev, preprod y prod 40 s
Carga 1 Preproducción, nocturna 20 min No: informe

La forma de la pirámide no es estética: la base es ancha porque las pruebas de abajo son baratas, rápidas y precisas, y la cima es estrecha porque las de arriba son lentas, caras y frágiles. Un equipo con 200 pruebas de extremo a extremo y 20 unitarias tiene una suite que tarda una hora, falla de forma intermitente y no dice dónde está el problema.

Las pruebas de contrato son el nivel que casi nadie tiene y el que este cambio demuestra necesario: cuestan 15 segundos y son lo único que detecta que un consumidor ajeno se va a romper.

Métricas DORA: MercadoFresco antes y después

Las cuatro métricas DORA miden la capacidad de entrega de un equipo. Las de MercadoFresco, medidas sobre los tres meses anteriores al módulo 8 y sobre los dos posteriores:

Métrica Antes (mayo-julio) Después (agosto-septiembre) Categoría
Frecuencia de despliegue 1,2 por semana 4,8 por semana De media a alta
Tiempo de entrega (commit → producción) 6 días y 4 h 3 h 20 min De media a alta
Tasa de fallo de cambios 23 % 7 % De baja a alta
Tiempo de restauración 47 min (mediana) 4 min De media a élite

Tres lecturas que conviene hacer sobre estos números, porque las cifras solas engañan:

La frecuencia sube porque el coste marginal de desplegar baja. Antes, desplegar costaba a Luis una hora de trabajo y una dosis de nervios, así que acumulaba cambios: «ya que despliego, que vayan los cuatro». Ahora cuesta cero, así que despliega en cuanto algo está listo. Y eso realimenta la tasa de fallo: un despliegue de un cambio es mucho más fácil de diagnosticar que uno de cuatro.

El tiempo de restauración es la mejora más grande y la más importante. De 47 minutos a 4 no es una mejora de proceso, es un cambio de naturaleza: 47 minutos de tienda degradada un viernes a las 19:00 son unos 700 pedidos afectados; 4 minutos son unos 60. Y la mayor parte de esos 4 minutos es detección, no reacción, porque la reversión en sí tarda 90 segundos.

La tasa de fallo del 7 % no es cero y no debería serlo. Un equipo con 0 % de fallos casi siempre está desplegando demasiado poco y demasiado tarde. El objetivo no es no fallar nunca, es que fallar sea barato: que un fallo cueste 4 minutos y no 47.

Y una advertencia. Las métricas DORA son útiles como termómetro del equipo y peligrosas como objetivo individual: en cuanto «frecuencia de despliegue» pasa a ser una meta, aparecen despliegues vacíos para subir el número. Mídelas, mira la tendencia, y no las cuelgues en un tablero de rendimiento personal.

Runbook de reversión y el fallo que se detecta tarde

Los mecanismos automáticos cubren los primeros quince minutos. El caso difícil es el fallo que aparece a las 19:30 de un despliegue de las 11:00, cuando el entorno azul ya no existe y la puerta de calidad dio el visto bueno hace horas.

Este es el runbook, pensado para seguirse a las tres de la mañana:

1. Contener (0-5 min). ¿Se puede apagar sin desplegar? Si el cambio está detrás de una bandera de funcionalidad, desactivarla es cuestión de segundos y es siempre la primera opción. Si no, deshabilitar la transición del pipeline hacia producción con disable-stage-transition para que nadie empeore la situación mientras se diagnostica.

2. Decidir la dirección (5-10 min). Es la decisión que más se equivoca bajo presión:

Situación Dirección Por qué
Solo cambió el código Atrás: desplegar la versión anterior Rápido y seguro
Hubo migración de expansión Atrás: la columna sobra pero no molesta Inocua por diseño
Hubo migración de contracción Adelante: corregir y desplegar Volver atrás rompería más
El evento subió de versión Depende de si hay consumidores actualizados Ver más abajo

3. Ejecutar (10-20 min). Hacia atrás es un despliegue normal de la versión anterior por el pipeline, usando el artefacto que sigue en S3 con su ruta predecible —tienda/1.5.2-8b2e440/paquete.zip—, lo que hace la reversión posible en un minuto. Es la razón de que la regla de ciclo de vida del bucket sea de 60 días y no de 3.

4. Verificar (20-30 min). /salud devuelve la versión esperada, TiempoConfirmacionPedido vuelve a su franja normal, PedidosConfirmados recupera el ritmo, y la DLQ deja de crecer.

5. Recuperar los datos, el paso que se olvida y el que más daño deja. Los mensajes acumulados en mercadofresco-pedidos-fallidos se procesan con el runbook de la DLQ de 07-05 —contener, clasificar, corregir, reprocesar—, nunca con un redrive antes de corregir la causa. Y si el evento había subido de versión, lo publicado durante el incidente se reproduce desde el archivo de EventBridge, siempre con FilterArns para no repetir efectos que ya ocurrieron.

6. Post-mortem sin culpables, en 48 horas. Qué pasó, por qué las puertas no lo detectaron y qué prueba nueva lo habría detectado. Ese último punto es el único entregable obligatorio: un post-mortem que no produce una prueba nueva es una reunión.

Despliegues pequeños y banderas de funcionalidad

Dos prácticas que multiplican el valor de todo lo anterior.

Despliegues pequeños y frecuentes. No es una preferencia estética: es aritmética del diagnóstico. Un despliegue con un cambio tiene un sospechoso; uno con seis tiene quince parejas posibles de interacción. La regla de MercadoFresco es que una rama vive tres días como máximo —de 08-01— y que si un cambio no cabe, se parte en incrementos compatibles hacia atrás, exactamente como se ha partido este.

Banderas de funcionalidad con AWS AppConfig. Separan desplegar de publicar: el código sale a producción apagado, y se enciende cuando y para quien se decida.

# app/config.py
import boto3, json, time
cliente = boto3.client("appconfigdata")
_cache, _expira, _token = {}, 0, None

def bandera(nombre, por_defecto=False):
    """Lee AppConfig con cache de 60 s. Ante error, devuelve el valor por defecto."""
    global _cache, _expira, _token
    try:
        if time.time() > _expira:
            if _token is None:
                _token = cliente.start_configuration_session(
                    ApplicationIdentifier="mercadofresco",
                    EnvironmentIdentifier="produccion",
                    ConfigurationProfileIdentifier="banderas",
                    RequiredMinimumPollIntervalInSeconds=60)["InitialConfigurationToken"]
            r = cliente.get_latest_configuration(ConfigurationToken=_token)
            _token = r["NextPollConfigurationToken"]
            contenido = r["Configuration"].read()
            if contenido:
                _cache = json.loads(contenido)
            _expira = time.time() + 60
        return _cache.get(nombre, {}).get("enabled", por_defecto)
    except Exception:
        return por_defecto      # una bandera rota NUNCA debe tumbar la tienda

Con esto, el proceso de compra hace if bandera("franja_entrega_activa"): y Luis despliega el martes con la bandera apagada, la enciende el miércoles al 10 % de clientes, mira las métricas y llega al 100 % el jueves. Desplegar deja de ser el momento de riesgo.

Aspecto Despliegue canario Bandera de funcionalidad
Qué controla Qué versión del código corre Qué comportamiento se activa
Granularidad % de peticiones Por cliente, región o segmento
Tiempo de apagado 90 s (reversión) Segundos, sin desplegar
Duración razonable Minutos Días o semanas
Coste si se abusa Ninguno Deuda técnica: ramas muertas en el código

La última fila es la advertencia. Una bandera es código temporal con fecha de caducidad: hay que apuntarla en el backlog el mismo día que se crea y borrarla en cuanto esté al 100 % y estable. Un sistema con cuarenta banderas antiguas tiene un espacio de estados que nadie puede razonar ni probar.

El viernes por la tarde como decisión de negocio

«No se despliega en viernes» es una de las reglas más repetidas del sector, y casi siempre por el motivo equivocado. El motivo habitual es el miedo: no sabemos qué pasará y no queremos estar aquí para verlo. Eso no es una política, es un síntoma —de que el pipeline no da confianza, de que la reversión no está probada, de que nadie sabe cuánto se tarda en volver atrás—.

Con lo que MercadoFresco tiene ahora, la pregunta se puede responder con números:

Factor Lunes 10:00 Viernes 19:00
Pedidos/hora ~180 ~900
Pedidos afectados por 4 min de degradación ~12 ~60
Personas disponibles para responder 3 1
Tiempo hasta la siguiente ventana 1 h 60 h

La regla de MercadoFresco no es «no se despliega en viernes», es «no se despliega a producción entre las 16:00 y las 22:00 de los viernes». Y la razón no es el miedo: es que el mismo incidente cuesta cinco veces más en esa franja y hay un tercio del equipo para atenderlo. Es una decisión de negocio, tomada con datos, revisable si los datos cambian. Fuera de esa franja se despliega el viernes como cualquier otro día, incluida la tarde del jueves y la mañana del viernes.

Y se implementa como control, no como recordatorio: una transición deshabilitada de forma programada, para que no dependa de que alguien se acuerde a las 18:50.

Cierre del módulo y coste

El cuarto problema del curso queda resuelto. El módulo empezó con el código de MercadoFresco en un portátil, un scp por SSH un viernes por la tarde y ninguna forma de volver atrás salvo buscar el fichero anterior. Termina con un cambio que toca cuatro sistemas recorriendo el camino completo sin que nadie escriba un comando.

Problema del curso Módulo Estado
1. Servidor único que no aguanta el pico 2-3 Resuelto: ASG, ALB, CloudFront
2. Seguridad y secretos 4 Resuelto: IAM, KMS, Secrets Manager, WAF
3. Confirmación de pedido de 2.893 ms 6-7 Resuelto: 400 ms con colas y eventos
4. Despliegues arriesgados 8 Resuelto
5. Infraestructura creada a mano 9 Pendiente

La cadena completa, en una frase por eslabón: 08-01 puso el código en un repositorio con ramas cortas, pull requests y protección de main. 08-02 convirtió cada push en una construcción limpia que prueba, escanea y produce un artefacto inmutable. 08-03 convirtió el despliegue en una operación gobernada con ganchos, estrategias graduales y reversión automática ante alarma. 08-04 encadenó todo en un pipeline con etapas, aprobación informada y puertas de calidad. Y 08-05 ha demostrado que la cadena aguanta un cambio de verdad, con esquema y contrato incluidos.

Concepto Coste mensual
CodeCommit / GitHub / CodeConnections / AppConfig 0,10 USD
CodeBuild (~1.850 min) 20,90 USD
CodeDeploy (capacidad blue/green) 2,00 USD
CodePipeline V2 3,50 USD
Artefactos en S3 y registros 1,30 USD
Total del módulo 8 ≈ 27,80 USD/mes

Veintiocho dólares al mes. La comparación correcta no es contra cero: es contra 47 minutos de mediana de restauración, un 23 % de despliegues que fallaban y las horas que Luis dedicaba a desplegar a mano.

Errores Comunes y Consejos

Desplegar el productor antes que el consumidor. Es el incidente del módulo 7 y la regla es una sola frase: primero el que lee, después el que escribe.

Meter el cambio de esquema en el mismo despliegue que el código. La reversión revierte artefactos, no bases de datos.

Una migración de expansión con NOT NULL o con índice. Deja de ser inocua: rompe la versión anterior o bloquea la tabla.

Probar solo la compatibilidad hacia atrás. Durante un despliegue gradual existen las dos direcciones a la vez; hacen falta las dos pruebas.

Quitar o renombrar un campo de un evento. No es compatible aunque el consumidor «parezca» tolerante. Añadir, publicar ambos, esperar, quitar.

Reconstruir el artefacto antes de producción. Invalida todas las pruebas y la aprobación anteriores.

Configuración distinta puesta a mano en una instancia. Desaparece en el siguiente blue/green y vuelve como un fallo intermitente indiagnosticable.

Banderas de funcionalidad que nunca se retiran. Cuarenta banderas antiguas son un espacio de estados que nadie puede probar. Apúntala en el backlog el día que la creas.

Una bandera que tumba la tienda si AppConfig falla. El valor por defecto ante error debe ser siempre seguro.

Usar las métricas DORA como objetivo individual. Aparecen despliegues vacíos y la métrica deja de medir nada.

Redrive de la DLQ antes de corregir la causa. Los mensajes vuelven a fallar y se pierde información.

Consejo: escribe la sección «Riesgos» de la PR antes de escribir el código. Si no sabes qué riesgos tiene tu cambio, todavía no lo has diseñado. Y ensaya la reversión a propósito una vez al trimestre, cronometrándola: es la única forma de que el número del runbook sea cierto.

Consejo: en el post-mortem, el entregable obligatorio es una prueba nueva. Sin eso, es una reunión.

Consejo: convierte «no desplegamos los viernes» en una ventana con datos. Una regla que se justifica con números se respeta; una que se justifica con miedo se salta el día que hay prisa.

Ejercicios

Ejercicio 1: el cambio que no cabe en tres días

Sara pide ahora algo mayor: entregas programadas hasta con 7 días de antelación, con calendario de disponibilidad por código postal, precios distintos según el día y un límite de pedidos por franja y zona. Toca la tienda, la API, tres tablas nuevas en Aurora, el consumidor de almacén, el evento PedidoConfirmado (que necesita fecha_entrega además de franja_entrega) y un cálculo de precio nuevo. Luis estima tres semanas.

Descomponlo en incrementos que respeten el límite de tres días por rama. Para cada uno indica qué se despliega, qué cambia en el esquema y en qué fase, si necesita bandera de funcionalidad y qué se puede verificar en producción al terminarlo. Di explícitamente cuál es el primero y por qué.

Ejercicio 2: el fallo de las 19:30

Martes, 11:00: se despliega la 1.6.0 con todo en verde. A las 19:30, Sara avisa de que en el panel mercadofresco-negocio los pedidos con franja tarde han caído a cero desde las 17:00, mientras que manana y 24h van normales. PedidosConfirmados total está solo un 8 % por debajo de lo normal, ninguna alarma ha saltado y la puerta de calidad dio el visto bueno a las 11:20. El entorno azul ya no existe.

Responde: (a) por qué ningún mecanismo automático lo detectó; (b) los primeros 15 minutos, en orden; (c) hacia dónde revertir y por qué, sabiendo que la migración fue de expansión; (d) qué se hace con los pedidos de esas dos horas y media; (e) qué tres controles nuevos saldrían del post-mortem.

Ejercicio 3: medir y mejorar DORA

MercadoFresco lleva dos meses con el pipeline. Datos del último mes: 19 despliegues a producción; tiempo de entrega mediano de 3 h 20 min, del que 2 h 40 min es espera de la aprobación de Marta; 2 despliegues revertidos de 19; tiempo de restauración mediano de 4 min, pero con un caso de 95 minutos —el fallo detectado tarde—. Marta quiere llegar a categoría élite en las cuatro métricas.

Para cada una: di dónde está MercadoFresco, cuál es el objetivo élite, qué cambio concreto propondrías y qué riesgo tiene ese cambio. Indica cuál abordarías primero y justifícalo.

Soluciones

Solución 1

Seis incrementos, cada uno desplegable y verificable por separado. El principio que los ordena es que cada incremento debe ser útil o inocuo por sí solo, nunca «media funcionalidad a medio camino».

# Qué se despliega Esquema Bandera Verificable al terminar
1 Tablas de calendario y disponibilidad, vacías; nadie las lee EXPANDIR: 3 tablas nuevas No Las tablas existen y no afectan a nada
2 Carga del calendario y API interna de consulta Ninguno No /interno/disponibilidad?cp=08001 responde
3 fecha_entrega en pedido y evento (v3), opcional EXPANDIR: columna NULLABLE Sí, apagada Pedidos siguen funcionando sin fecha
4 Consumidor de almacén lee fecha_entrega con respaldo Ninguno No Procesa eventos con y sin fecha
5 Selector de fecha en el proceso de compra Ninguno Sí, al 10 % Un 10 % de clientes ve el calendario
6 Precio por día y límite por franja y zona EXPANDIR: columna de precio Precios correctos con la bandera activa

El primero es el incremento 1, y la razón es la regla de esta lección: el cambio de esquema es lo único que no revierte con el código, así que va solo, en su propio despliegue, sin nada más que pueda fallar y obligar a una reversión. Tres tablas vacías que nadie consulta son el cambio más seguro que existe: si algo va mal, no hay nada que deshacer. Meterlas en el mismo despliegue que el código nuevo sería mezclar el riesgo irreversible con el reversible, que es justo lo que 08-03 advierte.

Dos observaciones más. El incremento 4 va antes que el 5 por la regla del orden: el consumidor entiende antes de que el productor empiece a publicar. Y el 3 despliega el evento v3 con la bandera apagada, de modo que el campo existe en el código pero no se publica hasta que se encienda: eso da una ventana para verificar los consumidores en producción sin haber cambiado nada de verdad.

Solución 2

(a) Porque ninguna alarma medía la dimensión que falló. PedidosConfirmados es un agregado, y una caída del 8 % en el total está dentro del ruido normal de un martes; la alarma de latencia no salta porque el sistema no está lento, está rechazando; la DLQ no crece porque probablemente no hay excepción, sino una validación que devuelve un 400 limpio. Y la puerta de calidad hizo su trabajo correctamente a las 11:20, seis horas antes de que el problema apareciera: era una franja horaria con poco tráfico de tipo tarde.

La lección de fondo es incómoda y vale para cualquier sistema: las puertas automáticas detectan lo que se les ha dicho que miren, en la ventana en la que miran. Un fallo que solo afecta a un segmento y solo se manifiesta seis horas después es invisible para ellas por construcción. Lo detectó Sara mirando un panel de negocio, y eso no es un fracaso del sistema: es la razón por la que existe mercadofresco-negocio.

(b) Los primeros 15 minutos. Contener: si franja_entrega está tras una bandera de AppConfig, apagarla —los clientes vuelven al comportamiento anterior en segundos y el incidente termina ahí—. Si no, deshabilitar la transición hacia producción para que nadie despliegue encima. Confirmar: comprobar en CloudWatch que la caída de tarde es real y no un problema del panel, y mirar los registros de la API filtrando por peticiones con franja_entrega=tarde —lo más probable es un 400 o un 422 sistemático—. Acotar: ¿fallan todos los pedidos de tarde o solo algunos? ¿desde las 17:00 exactas? Que empiece a las 17:00 y no a las 11:00 es la pista principal: sugiere una condición horaria, por ejemplo una validación que rechaza la franja tarde cuando ya ha pasado su hora de corte, con la zona horaria mal calculada. Avisar: al equipo y a Sara, con lo que se sabe y lo que no.

(c) Hacia atrás, a la 1.5.2, si la bandera no existe o no resuelve. Y se puede hacer sin miedo precisamente porque la migración fue de expansión: la columna franja_entrega es NULLABLE, la 1.5.2 no la conoce y la ignora, así que revertir el código deja el sistema exactamente como estaba antes del martes. Los pedidos de las últimas horas conservan su franja en la columna, que se recuperará al desplegar la versión corregida. Si la migración hubiera sido de contracción, la respuesta sería la contraria —hacia adelante, corrigiendo— y con la tienda degradada mientras tanto: la fase de expansión es lo que convierte esta decisión en fácil.

(d) Los pedidos de esas dos horas y media. No están en la DLQ, porque probablemente ni llegaron a crearse: son clientes que intentaron elegir tarde, vieron un error y se fueron. Es una pérdida de ventas, no de datos, y hay que tratarla como tal: estimar el volumen comparando con la misma franja de los martes anteriores, y decidir con Sara si procede alguna acción comercial. Si además hubiera pedidos creados con la franja mal asignada, se identifican por rango temporal y se corrigen con un script revisado, nunca con un UPDATE improvisado en producción. Y si hay eventos que no se publicaron, se recuperan del archivo de EventBridge con FilterArns.

(e) Tres controles del post-mortem. Una alarma por dimensión, no solo por agregado: una métrica PedidosConfirmados con dimensión FranjaEntrega y una alarma que salte si cualquier franja cae a cero durante 15 minutos en horario comercial. Es barata y habría avisado a las 17:15 en lugar de a las 19:30. Una prueba de humo con reloj: los casos que dependen de la hora hay que probarlos a horas distintas, así que una ejecución sintética cada hora con las tres franjas, o pruebas unitarias con el reloj simulado cubriendo los bordes —y muy en particular la zona horaria, que aquí es la sospechosa—. Y extender la puerta de calidad con una segunda evaluación diferida: una comprobación automática a las 4 y a las 12 horas del despliegue que compare las métricas por dimensión con el mismo periodo de la semana anterior y avise si algo se ha movido. No bloquea el pipeline —ya terminó— pero convierte «lo vio Sara por casualidad» en «avisó el sistema».

Solución 3

Frecuencia de despliegue. MercadoFresco está en 19 al mes, unos 4,4 por semana: categoría alta. La élite es bajo demanda, varias veces al día. El cambio: quitar la aprobación manual —que es lo que impone un ritmo diario— y agrupar menos. El riesgo: quitarla sin que la puerta de calidad esté madura cambia un control por una hipótesis, y las condiciones de salida que Marta escribió en 08-04 existen precisamente para eso.

Tiempo de entrega. 3 h 20 min es alta; la élite es menos de una hora. Y el diagnóstico está servido: 2 h 40 min de las 3 h 20 son espera humana, así que el pipeline técnico ya tarda 40 minutos y está en rango élite. El cambio no es optimizar la construcción: es la aprobación. Dos medidas graduales antes de quitarla del todo: aprobación desde Slack con AWS Chatbot, que baja la espera de horas a minutos porque Marta no necesita estar delante del ordenador, y un suplente designado. El riesgo de la primera es prácticamente nulo, y por eso es la que hay que hacer ya.

Tasa de fallo de cambios. 2 de 19 es un 10,5 %: categoría media-alta; la élite está por debajo del 15 %, así que técnicamente ya está. Pero el número engaña sin mirar los casos: uno de los dos fallos fue el de las 19:30, que no lo detectó ninguna puerta. El cambio útil no es bajar el porcentaje, es mejorar la detección con los tres controles del ejercicio 2. El riesgo de perseguir el número por sí mismo es el de siempre: se dejan de desplegar cambios pequeños para no arriesgar la estadística.

Tiempo de restauración. Mediana de 4 minutos, que es élite, pero con un caso de 95 minutos. Aquí la mediana miente: lo que duele es la cola. El cambio: banderas de funcionalidad en todo cambio de comportamiento visible, que convierten la restauración en segundos sin desplegar, y la alarma por dimensión que reduce el tiempo de detección, que es donde estaban 90 de esos 95 minutos.

Primero, la aprobación desde Slack. Es la que más impacto tiene —2 h 40 min de 3 h 20—, la más barata —una integración de una tarde—, la de menor riesgo —no elimina ningún control, solo lo hace más accesible— y la que además mejora indirectamente la frecuencia, porque la espera de la aprobación es lo que empuja a agrupar cambios. Empezar por quitar la aprobación sería atacar la misma métrica con mucho más riesgo y sin haber cumplido las condiciones que el propio equipo se puso.

Conclusión

Un cambio de Luis ha recorrido el camino completo. Empezó como una petición de Sara sobre el 31 % de incidencias de reparto y terminó en producción tres horas después, tocando el código de la tienda, un consumidor de cola-mercadofresco-pedidos, el esquema de Aurora y el contrato de PedidoConfirmado, sin que nadie escribiera un comando. La rama duró dos días, la PR se revisó con la construcción en verde, el artefacto se construyó una vez y ese mismo objeto recorrió los tres entornos, Marta aprobó en cuarenta segundos con la información delante, y la puerta de calidad confirmó con métricas reales que la tienda seguía sana.

Lo que hace que esto funcione no son los servicios, son las dos disciplinas que esta lección ha puesto en práctica. La primera es la compatibilidad: la migración en fase de expansión con la columna NULLABLE, sin NOT NULL, sin DEFAULT y sin índice, que convierte un cambio irreversible en uno inocuo; y el evento versionado con un atributo opcional, con las dos pruebas de contrato que casi nadie escribe —el consumidor viejo con el evento nuevo y el consumidor nuevo con el evento viejo—, porque durante un despliegue gradual las dos situaciones existen a la vez. La segunda es el orden: primero el que lee, después el que escribe, declarado como runOrder en el pipeline y no como algo que alguien debe recordar. Ahí queda cerrado el incidente que el módulo 7 dejó abierto.

Tienes la pirámide de pruebas con sus niveles bloqueantes y el que casi nadie tiene —las de contrato, quince segundos que son lo único que detecta que un consumidor ajeno se va a romper—; la configuración por entorno en Parameter Store y los secretos en Secrets Manager, con la regla de que un valor puesto a mano en una instancia desaparece en el siguiente blue/green y vuelve como un fallo indiagnosticable; y el runbook de reversión con su decisión más difícil, la dirección: atrás si solo cambió código o hubo expansión, adelante si hubo contracción.

Y tienes los números. Frecuencia de despliegue de 1,2 a 4,8 por semana. Tiempo de entrega de 6 días a 3 horas. Tasa de fallo del 23 % al 7 %. Tiempo de restauración de 47 minutos a 4. Con las tres lecturas que los acompañan: que la frecuencia sube porque el coste marginal de desplegar baja, que la restauración es la métrica que de verdad cambia la naturaleza del riesgo, y que un 0 % de fallos sería señal de estar desplegando demasiado poco —el objetivo no es no fallar, es que fallar cueste cuatro minutos y no cuarenta y siete—. Más las banderas de funcionalidad con AppConfig, que separan desplegar de publicar y tienen su propia deuda si nadie las retira, y la conversión de «no desplegamos los viernes» en una ventana justificada con datos: cinco veces más pedidos en juego y un tercio del equipo disponible. Una decisión de negocio, no de fe.

El cuarto problema del curso queda resuelto, y por unos 28 dólares al mes. MercadoFresco tiene un código gobernado, una construcción que verifica, un despliegue que se revierte solo y un pipeline que lo encadena todo.

Pero hay una asimetría difícil de justificar y que esta lección ha dejado a la vista dos veces. El código de la aplicación está gobernado; la infraestructura no. La VPC se creó con create-vpc en un terminal. Las colas con create-queue. Las reglas de EventBridge subiendo un JSON. Las alarmas, los grupos de seguridad, los grupos de destino del ALB, las tablas de DynamoDB: todo existe porque alguien escribió un comando algún día, y en ningún sitio hay un registro de por qué. Nadie puede recrear el entorno de MercadoFresco desde cero. Nadie puede garantizar que preproducción tenga la misma configuración que producción —de hecho no la tiene, y la diferencia se descubre en cada incidente—. Y el propio pipeline que existe para que nadie despliegue a mano se creó a mano, con un JSON en el portátil de Luis. Los tres entornos comparten además la cuenta 111122223333, así que el radio de explosión no está acotado, las cuotas se comparten y la factura no se separa de verdad.

En el módulo 9, «Infraestructura como código y gobierno de cuentas», atacamos el quinto y último problema del curso. 09-01, «AWS CloudFormation», trae las plantillas declarativas, las pilas, los conjuntos de cambios que dicen qué va a pasar antes de que pase y la reversión de infraestructura. 09-02, «AWS CDK», permite definir esa misma infraestructura con código de verdad, con abstracciones y pruebas. 09-03, «Elastic Beanstalk», muestra la alternativa gestionada y cuándo compensa. Y 09-04, «AWS Organizations», resuelve la separación seria de entornos con cuentas distintas, políticas de control de servicios y facturación consolidada. Al final de ese módulo, todo lo que hemos construido en ocho módulos podrá recrearse desde cero con un comando.

Curso de AWS

Módulo 1: Introducción a AWS

Módulo 2: Servicios principales de AWS

Módulo 3: Redes y entrega de contenido

Módulo 4: Seguridad e identidad

Módulo 5: Monitorización y gestión

Módulo 6: Bases de datos

Módulo 7: Integración de aplicaciones

Módulo 8: Herramientas para desarrolladores

Módulo 9: Infraestructura como código y gobierno de cuentas

Módulo 10: Contenedores en AWS

Módulo 11: Mejores prácticas y gestión de costos

© Copyright 2026. Todos los derechos reservados