El fan-out de 07-02 funciona, pero tiene un techo que ya se nota. mercadofresco-pedido-confirmado transporta un único tipo de hecho; para PedidoCancelado, StockBajo y RepartoAsignado haría falta un tema por cada uno, cada uno con sus suscripciones, sus políticas de cola y sus filtros. Cuatro temas hoy, doce el año que viene, y un mapa de suscripciones que nadie sabe dibujar entero. Además hay hechos que interesan y que la tienda no publica —que una instancia de asg-mercadofresco-tienda cambie de estado, que Trusted Advisor detecte un recurso ocioso, que un despliegue falle— y hay una herida abierta: cuando la cola de analítica estuvo mal configurada, los eventos de esas horas se perdieron para siempre.

Amazon EventBridge es el bus de eventos sin servidor de AWS. En lugar de un canal por tipo de hecho, hay un bus donde todo se publica, y reglas que deciden a dónde va cada evento mirando su contenido completo. El emisor no conoce a nadie; el receptor no depende del emisor; lo que los une es un patrón JSON que se puede cambiar sin desplegar código. Y como bonus, EventBridge recibe de fábrica los eventos de más de 200 servicios de AWS y puede archivar y reproducir todo lo que pasa por él.

En esta lección Luis crea bus-mercadofresco, diseña el catálogo de eventos de negocio de la tienda, escribe patrones de complejidad creciente, conecta un destino de API hacia el ERP del almacén, programa la carga nocturna a Redshift con cron, y por fin explota los Streams de mercadofresco-carritos que quedaron presentados y sin usar en 06-02.

Aviso de coste. EventBridge cobra 1,00 USD por millón de eventos personalizados publicados; los eventos de servicios de AWS en el bus por defecto son gratuitos. El archivo cuesta por GB almacenado y la reproducción se cobra como publicación nueva: reproducir un mes de eventos vuelve a pasar por caja. Pipes y Scheduler tienen su propia tarifa. Al final tienes la limpieza. Datos ficticios.

Contenido

  1. SNS o EventBridge: la comparación honesta
  2. Buses: por defecto, personalizado y de socios
  3. Anatomía de un evento
  4. El catálogo de eventos de MercadoFresco
  5. Reglas y patrones de evento
  6. Destinos y transformación de la entrada
  7. Reintentos, edad máxima del evento y DLQ por destino
  8. Eventos de los servicios de AWS
  9. Reglas programadas y EventBridge Scheduler
  10. Registro de esquemas y enlaces de código
  11. Archivo y reproducción de eventos
  12. EventBridge Pipes y los Streams de mercadofresco-carritos
  13. Arquitectura dirigida por eventos: contratos y versionado
  14. Observabilidad y coste
  15. Errores comunes y consejos
  16. Ejercicios
  17. Conclusión

SNS o EventBridge: la comparación honesta

Los dos entregan un mensaje a varios destinos. La diferencia está en cuánto saben del contenido y en qué traen puesto de fábrica.

Amazon SNS Amazon EventBridge
Unidad Tema por tipo de mensaje Un bus para muchos tipos
Filtrado Atributos o cuerpo, reglas sencillas Patrón sobre todo el evento, anidado, arrays
Destinos por regla/suscripción 1 Hasta 5
Orígenes de AWS Solo los que publican explícitamente 200+ servicios de fábrica
Orígenes SaaS No Buses de socios (Datadog, Shopify, Zendesk…)
Esquemas No Registro y descubrimiento automático
Archivo y reproducción No
Transformación de la entrada No Sí (input transformer)
Latencia típica Decenas de ms Cientos de ms (~0,5 s)
Rendimiento Muy alto (>100.000/s) 10.000 publicaciones/s por defecto (ampliable)
Coste por millón 0,50 USD publicar + entregas 1,00 USD publicar, entregas incluidas
Correo, SMS, push No

El criterio práctico. Usa SNS cuando el patrón sea fan-out puro de un hecho a muchos destinos con filtros simples, cuando importe la latencia mínima, cuando el volumen sea muy alto, o cuando el destino sea una persona (correo, SMS). Usa EventBridge cuando haya varios tipos de evento sobre un mismo canal, cuando el enrutamiento dependa del contenido, cuando quieras reaccionar a eventos de AWS o de un SaaS, cuando necesites archivo y reproducción, o cuando el destino sea una API externa.

MercadoFresco acaba usando los dos, y no es una contradicción: bus-mercadofresco para los eventos de negocio y para todo lo que venga de AWS, y SNS para el fan-out de alto volumen de mercadofresco-pedido-confirmado —que ya funciona y tiene menos latencia— y para los avisos a personas de alertas-mercadofresco. De hecho, un tema de SNS puede ser destino de una regla de EventBridge, lo que permite combinarlos sin duplicar nada.

Buses: por defecto, personalizado y de socios

Un bus de eventos es un canal con nombre. Hay tres clases:

  • Bus por defecto (default). Existe en cada región sin crearlo. Aquí llegan automáticamente los eventos de los servicios de AWS de la cuenta: cambios de estado de EC2, resultados de Trusted Advisor, transiciones de tareas de ECS, fallos de despliegue. También acepta eventos propios, pero mezclarlos con el ruido de AWS complica las reglas y el archivo.
  • Bus personalizado. El que crea tu aplicación para sus eventos de negocio. Permite políticas de acceso propias, archivo propio y límites propios.
  • Bus de socio (partner event bus). Lo crea un SaaS asociado para enviarte sus eventos sin que montes webhooks: la pasarela de pago, el ERP en la nube o la herramienta de soporte.
export PERFIL="--profile mercadofresco-dev --region eu-west-1"

aws events create-event-bus --name bus-mercadofresco \
  --tags Key=Proyecto,Value=mercadofresco Key=Entorno,Value=produccion \
         Key=Componente,Value=integracion Key=Propietario,Value=marta \
         Key=CentroCoste,Value=tecnologia $PERFIL

MercadoFresco separa claramente: bus-mercadofresco para los eventos que publica la aplicación, y el bus default para reaccionar a lo que hace AWS.

Anatomía de un evento

Todo evento de EventBridge tiene el mismo sobre. Los campos que tú controlas son cuatro; el resto los pone el servicio.

{
  "version": "0",
  "id": "3f8a1c22-9e7d-4b10-a5f1-6c2b0e4d7a19",
  "detail-type": "PedidoConfirmado",
  "source": "mercadofresco.tienda",
  "account": "111122223333",
  "time": "2026-08-02T18:41:07Z",
  "region": "eu-west-1",
  "resources": ["arn:aws:rds:eu-west-1:111122223333:cluster:aurora-mercadofresco-pedidos"],
  "detail": {
    "version": 1,
    "pedido_id": "PED-2026-084417",
    "cliente_id": "CLI-30912",
    "importe_eur": 48.20,
    "franja_entrega": "24h",
    "provincia": "Barcelona",
    "lineas": [
      { "sku": "FRUT-FRES-011", "unidades": 2, "precio_eur": 3.10 },
      { "sku": "PESC-FRES-002", "unidades": 1, "precio_eur": 12.40 }
    ],
    "confirmado_en": "2026-08-02T18:41:07Z"
  }
}
Campo Quién lo pone Para qué sirve
source Espacio de nombres del emisor. Convención: <empresa>.<dominio>
detail-type Qué ha pasado. Es el campo que más se usa para enrutar
detail La carga útil, JSON libre
resources ARN de los recursos implicados; permite filtrar por recurso
id, time, region, account EventBridge Metadatos; time sirve para el archivo y la reproducción

Dos advertencias sobre el detail. Primera: el evento completo no puede pasar de 256 KB, igual que un mensaje de SQS; si el detalle es grande, guarda en S3 y manda la referencia. Segunda: detail es un contrato con terceros, no una estructura interna. Volveremos a ello al final de la lección.

El catálogo de eventos de MercadoFresco

Antes de escribir una sola regla, Marta y Luis escriben el catálogo. Es un documento de una página, y es la pieza de gobierno más rentable de todo el módulo.

source detail-type Cuándo se emite Campos clave del detail
mercadofresco.tienda PedidoConfirmado Cobro correcto y pedido escrito en Aurora pedido_id, cliente_id, importe_eur, franja_entrega, provincia, lineas
mercadofresco.tienda PedidoCancelado El cliente o el sistema anulan un pedido pedido_id, motivo, importe_devuelto_eur
mercadofresco.almacen StockBajo Un SKU baja del umbral de reposición sku, unidades_restantes, umbral, proveedor_id
mercadofresco.reparto RepartoAsignado Se asigna repartidor y franja pedido_id, repartidor_id, franja, eta

Tres convenciones que evitan mucho dolor. El detail-type va en pasado y describe un hecho, no una orden: PedidoConfirmado, no ConfirmarPedido; si describe una orden, has vuelto al acoplamiento que querías evitar. El source identifica al dominio emisor, no al equipo ni al servicio técnico, para que sobreviva a reorganizaciones. Y el detail lleva siempre version, que es lo único que permitirá evolucionar el contrato sin romper consumidores.

Publicar es una llamada:

import json, boto3
from datetime import datetime, timezone

eb = boto3.client("events", region_name="eu-west-1")


def publicar_pedido_confirmado(pedido):
    respuesta = eb.put_events(Entries=[{
        "EventBusName": "bus-mercadofresco",
        "Source": "mercadofresco.tienda",
        "DetailType": "PedidoConfirmado",
        "Time": datetime.now(timezone.utc),
        "Resources": ["arn:aws:rds:eu-west-1:111122223333:cluster:aurora-mercadofresco-pedidos"],
        "Detail": json.dumps({"version": 1, **pedido}, ensure_ascii=False),
    }])
    # put_events devuelve 200 aunque falle alguna entrada: hay que mirar FailedEntryCount.
    if respuesta["FailedEntryCount"]:
        for e in respuesta["Entries"]:
            if "ErrorCode" in e:
                log.error("evento no publicado",
                          extra={"code": e["ErrorCode"], "msg": e["ErrorMessage"]})
    return respuesta

put_events acepta hasta 10 entradas por llamada y tiene la misma trampa que send_message_batch de 07-01: 200 con fallos parciales dentro. Si no compruebas FailedEntryCount, pierdes eventos en silencio.

Reglas y patrones de evento

Una regla tiene un patrón y hasta cinco destinos. Un patrón de evento es un JSON con la misma forma que el evento, donde cada valor es una lista de alternativas. El evento encaja si todos los campos del patrón coinciden.

El patrón más simple: todos los pedidos confirmados.

{
  "source": ["mercadofresco.tienda"],
  "detail-type": ["PedidoConfirmado"]
}

A partir de ahí, el lenguaje crece. Estos son los operadores, aplicados al detalle del pedido:

Operador Patrón Qué selecciona
Coincidencia exacta {"detail":{"franja_entrega":["24h"]}} Franja urgente
Lista (OR) {"detail":{"provincia":["Barcelona","Girona"]}} Cualquiera de las dos
prefix {"detail-type":[{"prefix":"Pedido"}]} PedidoConfirmado y PedidoCancelado
suffix {"detail":{"lineas":{"sku":[{"suffix":"-BIO"}]}}} Alguna línea ecológica
anything-but {"detail":{"provincia":[{"anything-but":["Baleares","Canarias"]}]}} Todo menos las islas
numeric {"detail":{"importe_eur":[{"numeric":[">=",150]}]}} Pedidos grandes
Rango {"detail":{"importe_eur":[{"numeric":[">",50,"<=",200]}]}} Entre 50 y 200 €
exists {"detail":{"cupon":[{"exists":true}]}} Solo con cupón
cidr {"detail":{"ip":[{"cidr":"10.0.0.0/16"}]}} Origen interno
equals-ignore-case {"detail":{"provincia":[{"equals-ignore-case":"barcelona"}]}} Sin importar mayúsculas
$or Ver abajo OR entre campos distintos

Las reglas de composición son las mismas que en SNS y siguen siendo la fuente número uno de patrones que no encajan: dentro de un campo, la lista es OR; entre campos distintos, AND. Este patrón exige las dos cosas a la vez:

{
  "source": ["mercadofresco.tienda"],
  "detail-type": ["PedidoConfirmado"],
  "detail": {
    "franja_entrega": ["24h"],
    "importe_eur": [{ "numeric": [">=", 60] }]
  }
}

Y este selecciona urgentes o grandes, que es distinto:

{
  "source": ["mercadofresco.tienda"],
  "detail-type": ["PedidoConfirmado"],
  "$or": [
    { "detail": { "franja_entrega": ["24h"] } },
    { "detail": { "importe_eur": [{ "numeric": [">=", 60] }] } }
  ]
}

Los arrays tienen una semántica propia que hay que entender. El patrón {"detail":{"lineas":{"sku":[{"prefix":"PESC-"}]}}} encaja si alguna de las líneas empieza por PESC-. No hay forma de exigir que todas lo hagan: EventBridge evalúa arrays con semántica existencial. Si necesitas «todas», el filtro va en el destino, no en la regla.

Crear la regla y engancharle destinos:

aws events put-rule --name regla-mf-pedido-confirmado-almacen \
  --event-bus-name bus-mercadofresco --state ENABLED \
  --description "Enruta pedidos confirmados hacia la cola del almacen" \
  --event-pattern '{"source":["mercadofresco.tienda"],"detail-type":["PedidoConfirmado"]}' $PERFIL

aws events put-targets --rule regla-mf-pedido-confirmado-almacen \
  --event-bus-name bus-mercadofresco \
  --targets '[{
    "Id": "cola-almacen",
    "Arn": "arn:aws:sqs:eu-west-1:111122223333:cola-mercadofresco-almacen",
    "DeadLetterConfig": {"Arn":"arn:aws:sqs:eu-west-1:111122223333:mercadofresco-eventbridge-fallidas"},
    "RetryPolicy": {"MaximumRetryAttempts": 8, "MaximumEventAgeInSeconds": 3600}
  }]' $PERFIL

Probar un patrón sin publicar nada es posible y conviene hacerlo siempre:

aws events test-event-pattern \
  --event-pattern file://patron.json \
  --event file://evento-ejemplo.json $PERFIL
# → { "Result": true }

Destinos y transformación de la entrada

Una regla admite hasta cinco destinos, de más de 20 tipos. Los que usa MercadoFresco:

Destino Uso Nota
Cola SQS Trabajo asíncrono duradero El más habitual; permite MessageGroupId en FIFO
Función Lambda Reacción inmediata EventBridge gestiona el permiso lambda:InvokeFunction
Tema SNS Reutilizar el fan-out ya montado Une los dos mundos
Máquina de estados Arrancar el proceso de pedido Lo veremos en 07-04
Destino de API Llamar al ERP del socio por HTTPS Con autenticación y contrapresión gestionadas
Otro bus / otra cuenta Federar entornos Requiere política de bus en el destino
Firehose, Kinesis, Log group Volcado y auditoría A mercadofresco-registros-web

El destino de API merece atención porque resuelve un problema real. Llamar a una API externa desde una Lambda obliga a gestionar credenciales, reintentos y límites de ritmo a mano. Un destino de API lo hace EventBridge: guarda las credenciales en una conexión (que a su vez las deposita en Secrets Manager), añade la cabecera de autenticación, y limita las invocaciones por segundo.

aws events create-connection --name conexion-erp-almacen \
  --authorization-type API_KEY \
  --auth-parameters '{"ApiKeyAuthParameters":{"ApiKeyName":"x-api-key","ApiKeyValue":"FICTICIA-123"}}' \
  $PERFIL

aws events create-api-destination --name destino-api-erp-almacen \
  --connection-arn arn:aws:events:eu-west-1:111122223333:connection/conexion-erp-almacen/abc123 \
  --invocation-endpoint https://erp.socio.example/v1/pedidos \
  --http-method POST \
  --invocation-rate-limit-per-second 12 $PERFIL

--invocation-rate-limit-per-second 12 es exactamente el límite que el ERP aguanta. EventBridge no lo supera aunque lleguen 900 eventos de golpe, y el resto espera con reintentos.

La transformación de la entrada (input transformer) evita el acoplamiento entre el formato del evento y lo que el destino espera. Sin ella, el ERP tendría que entender el sobre completo de EventBridge y el nombre exacto de nuestros campos, y cualquier cambio en el evento rompería al socio.

{
  "InputPathsMap": {
    "pedido": "$.detail.pedido_id",
    "cliente": "$.detail.cliente_id",
    "franja": "$.detail.franja_entrega",
    "momento": "$.time"
  },
  "InputTemplate": "{\"orderRef\":\"<pedido>\",\"customerRef\":\"<cliente>\",\"deliverySlot\":\"<franja>\",\"createdAt\":\"<momento>\",\"channel\":\"web\"}"
}

InputPathsMap extrae valores con JSONPath y les da un alias; InputTemplate compone la carga que recibe el destino. El resultado es que el ERP recibe su propio vocabulario (orderRef, customerRef) sin que MercadoFresco tenga que renombrar nada en su evento. La traducción vive en la regla, que es el sitio barato de cambiar.

Reintentos, edad máxima del evento y DLQ por destino

Si un destino falla, EventBridge reintenta con retroceso exponencial durante 24 horas y hasta 185 intentos por defecto. Ambos límites se ajustan por destino, y el primero que se cumpla detiene los intentos:

  • MaximumRetryAttempts: número máximo de reintentos (0–185).
  • MaximumEventAgeInSeconds: antigüedad máxima del evento (60–86.400 s).

MaximumEventAgeInSeconds es el parámetro que más se olvida y el que más importa en negocio. Un aviso de reparto entregado 20 horas tarde no vale nada: es preferible que caiga a la DLQ a la hora y que alguien lo mire, en vez de que llegue cuando el reparto ya se hizo. Marta lo fija en 3.600 s para todo lo relacionado con pedidos.

La DLQ por destino (DeadLetterConfig) recoge lo que no se pudo entregar. Es una cola SQS normal, y el mensaje que llega incluye atributos con el motivo: RULE_ARN, TARGET_ARN, ERROR_CODE, ERROR_MESSAGE y EXHAUSTED_RETRY_CONDITION. Ese último campo es oro puro para el diagnóstico: dice si se agotaron los reintentos o si venció la edad máxima.

No confundas las tres DLQ que ya conviven en MercadoFresco:

DLQ Recoge Causa típica
De la cola SQS Lo que el consumidor no pudo procesar Datos inválidos, dependencia caída
De la suscripción SNS Lo que SNS no pudo entregar Permisos, endpoint caído
Del destino de EventBridge Lo que EventBridge no pudo entregar Permisos, destino saturado, edad vencida

Eventos de los servicios de AWS

Esta es la capacidad que SNS no tiene: más de 200 servicios de AWS publican en el bus default sin que configures nada. Solo hay que escribir la regla.

Una instancia del grupo de la tienda que se apaga o pasa a estado degradado:

{
  "source": ["aws.ec2"],
  "detail-type": ["EC2 Instance State-change Notification"],
  "detail": { "state": ["stopped", "terminated", "stopping"] }
}

Un hallazgo de Trusted Advisor (05-05), que hasta ahora había que ir a mirar a mano:

{
  "source": ["aws.trustedadvisor"],
  "detail-type": ["Trusted Advisor Check Item Refresh Notification"],
  "detail": { "status": ["WARN", "ERROR"] }
}

Una tarea de ECS que muere (módulo 10) o un despliegue de CodeDeploy que falla (08-03):

{
  "source": ["aws.ecs", "aws.codedeploy"],
  "detail-type": [
    "ECS Task State Change",
    "CodeDeploy Deployment State-change Notification"
  ],
  "detail": { "state": ["STOPPED"], "status": ["FAILURE"] }
}

Cuidado con ese último: al combinar dos orígenes en una regla, el patrón detail se aplica a ambos y puede no encajar con ninguno. Es más limpio una regla por origen, aunque parezca repetitivo: los patrones quedan legibles y cada una puede tener sus propios destinos y su propia DLQ.

MercadoFresco crea regla-mf-infra-incidentes con destino alertas-mercadofresco, y la transformación de entrada convierte el evento crudo en una frase legible para Marta:

{
  "InputPathsMap": { "recurso": "$.detail.instance-id", "estado": "$.detail.state" },
  "InputTemplate": "\"Instancia <recurso> ha pasado a estado <estado> en eu-west-1.\""
}

Este es un buen momento para señalar el cambio de mentalidad: la operación pasa de consultar a reaccionar. En lugar de que alguien revise Trusted Advisor cada lunes, un evento abre un ticket cuando hay algo que mirar.

Reglas programadas y EventBridge Scheduler

EventBridge también dispara por tiempo. Hay dos mecanismos y conviene saber cuál usar.

Las reglas programadas clásicas viven en el bus default y usan schedule-expression:

aws events put-rule --name regla-mf-limpiar-carritos \
  --schedule-expression "cron(0 3 * * ? *)" --state ENABLED \
  --description "Limpieza diaria de carritos caducados a las 03:00 UTC" $PERFIL

EventBridge Scheduler es el servicio dedicado, más nuevo y preferible para casi todo: soporta zonas horarias con horario de verano, ventanas de dispersión (flexible time windows) para no lanzar mil tareas en el mismo segundo, programaciones de una sola vez, y más de 270 destinos con los mismos reintentos y DLQ que las reglas.

aws scheduler create-schedule-group --name grupo-mf-programado $PERFIL

aws scheduler create-schedule --name programador-mf-carga-nocturna \
  --group-name grupo-mf-programado \
  --schedule-expression "cron(30 2 * * ? *)" \
  --schedule-expression-timezone "Europe/Madrid" \
  --flexible-time-window '{"Mode":"FLEXIBLE","MaximumWindowInMinutes":15}' \
  --target '{
    "Arn": "arn:aws:lambda:eu-west-1:111122223333:function:mercadofresco-carga-redshift",
    "RoleArn": "arn:aws:iam::111122223333:role/rol-mf-scheduler",
    "Input": "{\"origen\":\"aurora\",\"destino\":\"analitica.hechos_pedidos\",\"modo\":\"incremental\"}",
    "RetryPolicy": {"MaximumRetryAttempts": 3, "MaximumEventAgeInSeconds": 3600},
    "DeadLetterConfig": {"Arn":"arn:aws:sqs:eu-west-1:111122223333:mercadofresco-eventbridge-fallidas"}
  }' $PERFIL

El --schedule-expression-timezone "Europe/Madrid" es la razón principal para preferir Scheduler: con una regla clásica en UTC, la carga nocturna se ejecutaría a las 04:30 en invierno y a las 03:30 en verano, desalineada con el cierre contable. Y MaximumWindowInMinutes: 15 reparte el arranque para no crear un pico de conexiones contra aurora-mf-lector-2.

Regla programada EventBridge Scheduler
Zona horaria y horario de verano No (solo UTC)
Ventana flexible No
Programación de un solo uso No Sí (at(...))
Límite por cuenta 300 reglas por bus 1 millón de programaciones
Destinos ~20 tipos 270+
Coste Gratis 1,00 USD/millón de invocaciones

Las dos tareas nocturnas de MercadoFresco quedan así: programador-mf-carga-nocturna a las 02:30 hora peninsular carga los pedidos del día en analitica.hechos_pedidos de wg-mercadofresco-analitica, y programador-mf-limpiar-carritos a las 03:00 recorre y consolida lo que el TTL de mercadofresco-carritos ya ha ido caducando. Ojo: las expresiones cron de AWS tienen seis campos (minuto, hora, día del mes, mes, día de la semana, año) y no admiten * en día del mes y día de la semana a la vez —de ahí el ?—.

Registro de esquemas y enlaces de código

El registro de esquemas guarda la estructura de tus eventos en formato OpenAPI/JSON Schema. Con el descubrimiento activado en un bus, EventBridge infiere el esquema de lo que pasa por él y lo versiona automáticamente.

aws schemas create-discoverer \
  --source-arn arn:aws:events:eu-west-1:111122223333:event-bus/bus-mercadofresco $PERFIL
aws schemas list-schemas --registry-name discovered-schemas $PERFIL

De un esquema se generan enlaces de código (code bindings) para Python, Java o TypeScript: clases tipadas que representan el evento, de modo que el consumidor deja de hacer evento["detail"]["pedido_id"] con los dedos cruzados. En Python el valor añadido es menor que en Java, pero el registro sigue siendo útil por otro motivo: es la documentación viva del catálogo. Cuando márketing pregunte qué campos trae PedidoConfirmado, la respuesta es un esquema versionado, no un mensaje de chat. El descubrimiento cuesta unos 0,10 USD por millón de eventos procesados, así que conviene tenerlo activo solo mientras el catálogo evoluciona.

Archivo y reproducción de eventos

Esta es la capacidad que habría salvado el incidente de 07-02. Un archivo guarda copia de todos los eventos que pasan por un bus —opcionalmente filtrados por patrón— durante el tiempo que decidas.

aws events create-archive --archive-name archivo-mercadofresco-eventos \
  --event-source-arn arn:aws:events:eu-west-1:111122223333:event-bus/bus-mercadofresco \
  --retention-days 90 \
  --event-pattern '{"source":[{"prefix":"mercadofresco."}]}' $PERFIL

Y cuando algo va mal, se reproduce una ventana de tiempo hacia las reglas afectadas:

aws events start-replay --replay-name reproduccion-analitica-20260802 \
  --event-source-arn arn:aws:events:eu-west-1:111122223333:archive/archivo-mercadofresco-eventos \
  --event-start-time 2026-08-02T08:00:00Z \
  --event-end-time 2026-08-02T14:00:00Z \
  --destination '{
    "Arn": "arn:aws:events:eu-west-1:111122223333:event-bus/bus-mercadofresco",
    "FilterArns": ["arn:aws:events:eu-west-1:111122223333:rule/bus-mercadofresco/regla-mf-pedido-confirmado-analitica"]
  }' $PERFIL

FilterArns es imprescindible y es lo que separa una recuperación de un desastre: sin él, la reproducción dispara todas las reglas del bus, y volverías a cobrar tarjetas, a enviar correos y a avisar al almacén de pedidos de hace seis horas. Con él, solo se reprocesa la regla de analítica, que es la que se perdió los eventos.

Tres cosas más que hay que saber antes de usarlo en serio:

  • Los eventos reproducidos llevan replay-name en el sobre. Un consumidor puede distinguirlos y actuar en consecuencia —por ejemplo, no volver a notificar a nadie—.
  • El orden no se garantiza dentro de la reproducción, y los eventos llegan mucho más rápido que en su momento original. El consumidor debe ser idempotente y aguantar el ritmo.
  • Reproducir cuesta como publicar. Un mes de eventos reproducido son un mes de eventos facturados de nuevo, más el almacenamiento del archivo (unos 0,10 USD por GB y mes).

El archivo también sirve para algo menos dramático y muy útil: poblar un entorno de pruebas con tráfico real, reproduciendo una tarde de viernes contra un bus de desarrollo.

EventBridge Pipes y los Streams de mercadofresco-carritos

En 06-02 activamos DynamoDB Streams en mercadofresco-carritos y lo dejamos ahí, presentado y sin explotar. EventBridge Pipes es la pieza que faltaba: una tubería punto a punto entre un origen que sondea y un destino, con dos pasos opcionales en medio.

flowchart LR
    ORIG[(DynamoDB Streams<br/>mercadofresco-carritos)] --> FIL[Filtrado<br/>solo REMOVE por TTL]
    FIL --> ENR[Enriquecimiento<br/>Lambda: anade datos del cliente]
    ENR --> DEST{{bus-mercadofresco<br/>CarritoAbandonado}}
    DEST --> R1[regla: aviso a marketing]
    DEST --> R2[regla: cola de analitica]
    style FIL fill:#fff3cd
    style ENR fill:#cfe2ff

Los orígenes de Pipes son los que hay que sondear: DynamoDB Streams, Kinesis, SQS, Amazon MQ y Kafka. Los destinos son cualquiera de EventBridge. Y los dos pasos intermedios son la razón de su existencia:

  • Filtrado: descarta lo que no interesa antes de pagar por procesarlo. El stream de carritos genera un registro por cada UpdateItem —cientos de miles al día—, pero solo interesan los REMOVE provocados por el TTL, que son los carritos abandonados de verdad.
  • Enriquecimiento: llama a una Lambda, una API o una máquina de estados para completar el registro antes de entregarlo. El stream trae el CLIENTE#<id> pero no el correo ni el nombre; el enriquecimiento los busca en Aurora.
aws pipes create-pipe --name pipe-mf-carritos-abandonados \
  --role-arn arn:aws:iam::111122223333:role/rol-mf-pipes \
  --source arn:aws:dynamodb:eu-west-1:111122223333:table/mercadofresco-carritos/stream/2026-07-01T00:00:00.000 \
  --source-parameters '{
    "DynamoDBStreamParameters": {"StartingPosition":"LATEST","BatchSize":50,
                                 "MaximumBatchingWindowInSeconds":30},
    "FilterCriteria": {"Filters":[{"Pattern":"{\"eventName\":[\"REMOVE\"],\"userIdentity\":{\"principalId\":[\"dynamodb.amazonaws.com\"]}}"}]}
  }' \
  --enrichment arn:aws:lambda:eu-west-1:111122223333:function:mercadofresco-enriquecer-carrito \
  --target arn:aws:events:eu-west-1:111122223333:event-bus/bus-mercadofresco \
  --target-parameters '{"EventBridgeEventBusParameters":{"Source":"mercadofresco.carritos",
                        "DetailType":"CarritoAbandonado"}}' $PERFIL

El filtro tiene un detalle exquisito: userIdentity.principalId = dynamodb.amazonaws.com distingue los borrados hechos por el TTL de los que hace la aplicación al confirmar un pedido. Sin esa condición, márketing enviaría correos de «has olvidado algo» a clientes que acaban de comprar. Es un ejemplo perfecto de por qué el filtrado tiene que estar cerca del origen y entender el dominio.

Con esto se cierra un hilo que llevaba abierto desde el módulo 6: los 55 GB de carritos zombis que el TTL empezó a limpiar ahora, además, generan un evento de negocio aprovechable.

Arquitectura dirigida por eventos: contratos y versionado

Los eventos desacoplan, pero no gratis. Hay tres verdades que conviene aceptar pronto.

Un evento es una API pública. En cuanto lo publicas en un bus compartido, no sabes quién lo consume: ese es el objetivo. Y entonces no puedes cambiarlo a tu antojo. Quitar un campo, renombrarlo o cambiar su tipo rompe consumidores que ni siquiera sabes que existen, y lo descubrirás en producción.

Las reglas de evolución compatible son cortas: se pueden añadir campos opcionales; no se puede quitar ni renombrar un campo, ni cambiar su tipo, ni cambiar el significado de un valor existente, ni endurecer una restricción. Cuando haga falta un cambio incompatible, se publica una versión nueva en paralelo —"version": 2 en el detalle, o un detail-type nuevo PedidoConfirmadoV2— y se mantienen las dos hasta que los consumidores migren. Por eso version va en el detalle desde el primer evento: sin él, la migración no tiene por dónde empezar.

El acoplamiento no desaparece, se desplaza. Antes la tienda dependía del ERP; ahora todos dependen del formato del evento. Es un cambio muy favorable —el contrato es explícito, versionable y verificable con test-event-pattern— pero sigue siendo una dependencia. La disciplina que la mantiene sana es la del catálogo: nada se publica en bus-mercadofresco sin estar en la tabla, y nada se cambia sin subir la versión.

Una última advertencia sobre el diseño: los eventos describen hechos, los comandos ordenan acciones. Si tu evento se llama EnviarCorreoConfirmacion, has puesto una orden en un bus, y el emisor vuelve a saber qué debe pasar después. El evento correcto es PedidoConfirmado; que eso implique un correo es decisión del consumidor.

Observabilidad y coste

Métrica Qué indica Acción
TriggeredRules Reglas que encajaron Si es 0, el patrón no coincide
MatchedEvents Eventos que encajaron con alguna regla Comparar con lo publicado
FailedInvocations El destino rechazó la invocación Alarma: casi siempre permisos
InvocationsFailedToBeSentToDlq Ni siquiera se pudo escribir en la DLQ Alarma crítica
ThrottledRules Se superó el límite de invocaciones Pedir aumento de cuota
PutEventsFailedEntriesCount Entradas rechazadas al publicar Revisar el publicador

La combinación más útil para depurar es MatchedEvents = 0 con publicaciones no nulas: significa que ningún patrón encaja, y casi siempre es un source mal escrito o un detail-type con una mayúscula de diferencia —los patrones distinguen mayúsculas—.

Además, EventBridge se integra con CloudWatch Logs como destino: una regla con patrón amplio y destino un grupo de registros permite ver todo lo que pasa por el bus durante una investigación. Es caro dejarlo permanentemente, pero es la herramienta más rápida cuando algo no aparece donde debería.

Concepto Precio MercadoFresco
Eventos personalizados publicados 1,00 USD/millón 480.000/mes → 0,48 USD
Eventos de servicios de AWS Gratis ~90.000/mes → 0 USD
Reglas y destinos Gratis
Archivo (almacenamiento) ~0,10 USD/GB-mes 3,4 GB × 90 días → 0,34 USD
Reproducción 1,00 USD/millón Solo en incidentes
Descubrimiento de esquemas ~0,10 USD/millón 0,05 USD
Pipes ~0,40 USD/millón de peticiones 210.000 tras filtrar → 0,08 USD
Scheduler 1,00 USD/millón 60/mes → 0 USD
Total ~0,95 USD/mes

Los eventos no filtrados son lo que puede disparar la factura: si el pipe de carritos no filtrara los MODIFY, procesaría 5,4 millones de registros al mes en lugar de 210.000. Filtrar en el origen no es solo higiene de diseño; es la partida principal.

aws events remove-targets --rule regla-mf-pedido-confirmado-almacen \
  --event-bus-name bus-mercadofresco --ids cola-almacen $PERFIL
aws events delete-rule --name regla-mf-pedido-confirmado-almacen \
  --event-bus-name bus-mercadofresco $PERFIL
aws pipes delete-pipe --name pipe-mf-carritos-abandonados $PERFIL
aws scheduler delete-schedule --name programador-mf-carga-nocturna \
  --group-name grupo-mf-programado $PERFIL
aws events delete-archive --archive-name archivo-mercadofresco-eventos $PERFIL
aws events delete-event-bus --name bus-mercadofresco $PERFIL

Un bus no se puede borrar si tiene reglas con destinos: hay que quitar destinos, luego reglas, luego el bus. Es la causa habitual de ResourceInUseException en los scripts de limpieza.

Errores Comunes y Consejos

Patrón que no encaja por mayúsculas o por un source mal escrito. MatchedEvents a 0 sin ningún error. Usa test-event-pattern antes de desplegar; te ahorra horas. Y no confundas OR y AND: dentro de un campo, lista = OR; entre campos, AND; para OR entre campos, $or.

Esperar que un patrón sobre un array exija «todos». EventBridge evalúa arrays con semántica existencial: encaja si alguno de los elementos cumple.

No comprobar FailedEntryCount en put_events. Devuelve 200 con fallos parciales, igual que send_message_batch. Eventos perdidos en silencio.

Publicar los eventos de negocio en el bus default. Se mezclan con el ruido de AWS, las reglas se vuelven frágiles y el archivo se llena de eventos que no interesan. Usa un bus propio.

Dejar la edad máxima del evento en 24 horas. Un aviso de reparto entregado 20 horas tarde es peor que un fallo visible. Ajusta MaximumEventAgeInSeconds al valor de negocio.

Reproducir sin FilterArns. Dispara todas las reglas del bus y repite efectos que ya ocurrieron. Es el error más caro de esta lección.

Olvidar la DLQ del destino. Sin ella, lo que no se entrega desaparece sin dejar rastro.

Poner comandos en el bus. Si el detail-type es un verbo en imperativo, has reintroducido el acoplamiento que querías eliminar.

Cambiar el detail sin subir version. Rompes consumidores que no sabes que existen y te enteras en producción.

Consejo: una regla por intención, no una regla con cinco destinos heterogéneos. Las reglas separadas tienen su propia DLQ, su propia edad máxima y su propia métrica, y se pueden desactivar de una en una durante un incidente.

Consejo: escribe el catálogo antes que el código. La tabla de source / detail-type / campos es diez minutos de trabajo que evitan meses de eventos incoherentes.

Consejo: activa el archivo desde el primer día. Cuesta céntimos y es la única forma de recuperar lo que un consumidor mal configurado se perdió.

Ejercicios

Ejercicio 1: enrutar el catálogo completo

Diseña las reglas de bus-mercadofresco para estos cuatro requisitos: (1) todos los PedidoConfirmado van a cola-mercadofresco-almacen; (2) los PedidoConfirmado de franja 24h o de importe ≥ 100 € van además al destino de API del ERP con prioridad; (3) los StockBajo de productos frescos (SKU que empiezan por FRUT- o PESC-) con menos de 5 unidades avisan a alertas-mercadofresco; (4) cualquier evento de mercadofresco.* se archiva 90 días.

Escribe los patrones JSON, di cuántas reglas creas y por qué, y qué transformación de entrada usarías en el requisito 3.

Ejercicio 2: el consumidor que se perdió una tarde

El martes a las 09:00 Luis despliega una versión de la Lambda de analítica que lanza una excepción al arrancar. Nadie lo detecta hasta las 15:00. La regla regla-mf-pedido-confirmado-analitica entrega directamente a esa Lambda, sin cola. Hay archivo activo desde hace tres meses.

Responde: (a) qué ha pasado con los eventos de esas seis horas según la configuración de reintentos por defecto y según una edad máxima de 3.600 s; (b) cómo recuperas los datos, con el comando exacto; (c) qué precaución tomas antes de lanzarlo; (d) qué dos cambios de arquitectura evitan que vuelva a pasar; (e) qué alarma habría avisado a las 09:05.

Ejercicio 3: SNS, EventBridge o Pipes

Decide el servicio y justifica en dos frases: (a) tres equipos quieren los pedidos confirmados, con filtros simples y la mínima latencia posible; (b) hay que llamar al ERP del socio por HTTPS con clave de API y como máximo 12 peticiones por segundo; (c) hay que avisar por SMS al técnico de guardia; (d) hay que lanzar la carga a Redshift todos los días a las 02:30 hora peninsular; (e) hay que reaccionar a los borrados por TTL de mercadofresco-carritos enriqueciendo con datos de Aurora; (f) hay que reaccionar a que una instancia EC2 pase a terminated.

Soluciones

Solución 1

Cuatro reglas más un archivo. Se separan porque cada una tiene destino, edad máxima y DLQ propios, y porque una regla con patrones mezclados se vuelve ilegible.

(1) regla-mf-pedido-confirmado-almacen:

{ "source": ["mercadofresco.tienda"], "detail-type": ["PedidoConfirmado"] }

(2) regla-mf-pedido-prioritario-erp. El OR entre campos distintos obliga a $or:

{
  "source": ["mercadofresco.tienda"],
  "detail-type": ["PedidoConfirmado"],
  "$or": [
    { "detail": { "franja_entrega": ["24h"] } },
    { "detail": { "importe_eur": [{ "numeric": [">=", 100] }] } }
  ]
}

Destino destino-api-erp-almacen con MaximumEventAgeInSeconds bajo (900 s: un pedido urgente que no llega en 15 minutos hay que tratarlo a mano) y DLQ mercadofresco-eventbridge-fallidas.

(3) regla-mf-stock-bajo-fresco. Aquí sí es AND entre campos: prefijo de SKU y unidades.

{
  "source": ["mercadofresco.almacen"],
  "detail-type": ["StockBajo"],
  "detail": {
    "sku": [{ "prefix": "FRUT-" }, { "prefix": "PESC-" }],
    "unidades_restantes": [{ "numeric": ["<", 5] }]
  }
}

Los dos prefix dentro de la lista de sku son un OR, que es justo lo que se pide. Transformación de entrada, porque el destino es un tema de SNS que acaba en el correo de una persona:

{
  "InputPathsMap": { "sku": "$.detail.sku", "quedan": "$.detail.unidades_restantes" },
  "InputTemplate": "\"Stock critico: quedan <quedan> unidades de <sku>. Reponer hoy.\""
}

(4) No es una regla, es un archivo con patrón {"source":[{"prefix":"mercadofresco."}]} y --retention-days 90. Conviene el prefijo para no archivar eventos de AWS que no aportan y sí ocupan.

Solución 2

(a) Con los valores por defecto (185 reintentos, 24 horas), los eventos de las 09:00 seguirían reintentándose a las 15:00, y al arreglar la Lambda una parte se entregaría sola, aunque con horas de retraso y en desorden. Con MaximumEventAgeInSeconds=3600, cada evento se descarta a la hora de su publicación: a las 15:00 todo lo anterior a las 14:00 está en la DLQ del destino y el resto sigue reintentando. La segunda configuración es preferible: convierte una degradación silenciosa en un montón visible de mensajes en la DLQ.

(b) Se arregla primero la Lambda y se comprueba con un evento de prueba. Después:

aws events start-replay --replay-name reproduccion-analitica-20260804 \
  --event-source-arn arn:aws:events:eu-west-1:111122223333:archive/archivo-mercadofresco-eventos \
  --event-start-time 2026-08-04T07:00:00Z --event-end-time 2026-08-04T13:00:00Z \
  --destination '{"Arn":"arn:aws:events:eu-west-1:111122223333:event-bus/bus-mercadofresco",
                  "FilterArns":["arn:aws:events:eu-west-1:111122223333:rule/bus-mercadofresco/regla-mf-pedido-confirmado-analitica"]}'

Las horas van en UTC: las 09:00–15:00 peninsulares de agosto son 07:00–13:00 UTC. Equivocarse aquí es un clásico.

(c) Tres precauciones. FilterArns obligatorio, o la reproducción dispararía todas las reglas y volvería a avisar al almacén y a facturar pedidos de hace seis horas. Verificar que el consumidor es idempotente, porque parte de los eventos pudo entregarse antes de fallar y la reproducción los repetirá. Y vaciar o revisar antes la DLQ del destino, para no reprocesar dos veces por dos caminos.

(d) Primero, meter una cola entre la regla y la Lambda: con SQS, seis horas de fallo dejan 5.000 mensajes esperando y se procesan al arreglar el problema sin reproducir nada; es la misma lección de 07-02 aplicada a EventBridge. Segundo, DLQ en el destino más alarma, para que la señal aparezca en minutos. Como tercera medida, un despliegue con verificación de salud que revierta solo, que es justamente el asunto del módulo 8.

(e) Una alarma sobre FailedInvocations de la regla, umbral > 0 en un periodo de 5 minutos, hacia alertas-mercadofresco. Complementaria: Errors de la función Lambda. Cualquiera de las dos habría avisado a las 09:05 en lugar de a las 15:00.

Solución 3

(a) SNS. Fan-out puro, filtros simples y requisito de latencia mínima: SNS entrega en decenas de milisegundos frente a los cientos de EventBridge, y ya está montado. Cada equipo con su cola suscrita.

(b) EventBridge con destino de API. Es exactamente su caso de uso: gestiona la clave en una conexión respaldada por Secrets Manager, aplica invocation-rate-limit-per-second 12 y reintenta con DLQ, todo sin escribir una línea de código.

(c) SNS. Es el único de los tres que entrega SMS. Además la alarma de CloudWatch ya publica ahí de forma nativa.

(d) EventBridge Scheduler. Necesita zona horaria con horario de verano (Europe/Madrid), que las reglas programadas clásicas no soportan, y la ventana flexible evita el pico de conexiones contra el lector de Aurora.

(e) EventBridge Pipes. Es el único que consume DynamoDB Streams con filtrado antes de facturar y con un paso de enriquecimiento integrado. Hacerlo con una Lambda suscrita al stream obligaría a filtrar y enriquecer a mano, procesando —y pagando— los millones de MODIFY que no interesan.

(f) EventBridge, bus default. Los eventos de EC2 llegan solos y son gratuitos; no hay nada que publicar. Una regla con patrón sobre aws.ec2 y destino alertas-mercadofresco.

Conclusión

MercadoFresco ya no tiene un tema por tipo de hecho, sino un bus donde conviven PedidoConfirmado, PedidoCancelado, StockBajo y RepartoAsignado, y donde reglas con patrones JSON deciden a dónde va cada uno mirando su contenido: la franja, el importe, la provincia, el prefijo del SKU. El ERP del socio recibe su propio vocabulario gracias a la transformación de entrada, sin que nadie renombre nada en el evento. Las tareas nocturnas —la carga a analitica.hechos_pedidos y la consolidación de carritos— corren con programador-mf-carga-nocturna en hora peninsular y con ventana flexible. Los incidentes de infraestructura y los hallazgos de Trusted Advisor llegan solos, sin que nadie tenga que ir a mirar. Y los Streams de mercadofresco-carritos, presentados en 06-02 y sin usar hasta hoy, alimentan por fin un evento de negocio a través de pipe-mf-carritos-abandonados, con un filtro que distingue el borrado del TTL del borrado por compra.

Dos ideas se llevan el peso de la lección. La primera es que el enrutamiento por contenido cambia quién decide: ya no es el emisor quien reparte trabajo, sino una regla declarativa que se modifica sin desplegar código. La segunda es que un evento es una API pública, con todo lo que eso implica: se puede añadir, no se puede quitar, y sin version en el detalle no hay migración posible. Entre medias queda archivo-mercadofresco-eventos, la red de seguridad que convierte «hemos perdido seis horas de datos» en una reproducción de un comando —siempre con FilterArns, o el remedio será peor que la enfermedad—.

Pero fíjate en lo que sigue sin resolver. Todo lo que hemos montado es coreografía: cada componente reacciona a lo que ve y nadie tiene la foto completa. Funciona de maravilla para hechos independientes, y se rompe cuando el proceso tiene pasos que dependen unos de otros y hay que deshacer lo hecho si algo falla a mitad. El pedido de MercadoFresco es exactamente eso: cobrar, reservar stock, pedir la preparación al almacén —que tarda entre dos minutos y una hora y la confirma una persona con un lector de códigos—, asignar reparto y confirmar al cliente. Si el almacén no puede preparar la caja porque el pescado ha llegado en mal estado, hay que devolver el cobro y liberar el stock, y con eventos sueltos nadie sabe en qué punto estaba el proceso ni qué toca compensar.

En 07-04, «AWS Step Functions», pasamos de la coreografía a la orquestación: una máquina de estados, mercadofresco-procesar-pedido, que conoce el proceso entero, guarda en qué paso va, reintenta con retroceso exponencial, espera a que un humano confirme mediante un token de retorno, procesa las 900 confirmaciones del viernes en paralelo con Map distribuido, y —lo más importante— sabe deshacer lo que ya había hecho cuando algo sale mal.

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