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
- SNS o EventBridge: la comparación honesta
- Buses: por defecto, personalizado y de socios
- Anatomía de un evento
- El catálogo de eventos de MercadoFresco
- Reglas y patrones de evento
- Destinos y transformación de la entrada
- Reintentos, edad máxima del evento y DLQ por destino
- Eventos de los servicios de AWS
- Reglas programadas y EventBridge Scheduler
- Registro de esquemas y enlaces de código
- Archivo y reproducción de eventos
- EventBridge Pipes y los Streams de
mercadofresco-carritos - Arquitectura dirigida por eventos: contratos y versionado
- Observabilidad y coste
- Errores comunes y consejos
- Ejercicios
- 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 | Sí |
| 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 | Sí | 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 $PERFILMercadoFresco 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 |
Tú | Espacio de nombres del emisor. Convención: <empresa>.<dominio> |
detail-type |
Tú | Qué ha pasado. Es el campo que más se usa para enrutar |
detail |
Tú | La carga útil, JSON libre |
resources |
Tú | 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 respuestaput_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.
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}
}]' $PERFILProbar 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" $PERFILEventBridge 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"}
}' $PERFILEl --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) | Sí |
| Ventana flexible | No | Sí |
| 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 $PERFILDe 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."}]}' $PERFILY 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"]
}' $PERFILFilterArns 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-nameen 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 losREMOVEprovocados 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"}}' $PERFILEl 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 $PERFILUn 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:
(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
- ¿Qué es AWS?
- Configuración de tu cuenta de AWS
- Infraestructura global de AWS
- Consola de administración de AWS
- AWS CLI y SDKs
Módulo 2: Servicios principales de AWS
Módulo 3: Redes y entrega de contenido
- Amazon VPC
- Grupos de seguridad y listas de control de acceso
- Elastic Load Balancing
- Amazon CloudFront
- Route 53
Módulo 4: Seguridad e identidad
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager y Parameter Store
- AWS Shield
- AWS WAF
Módulo 5: Monitorización y gestión
- Amazon CloudWatch
- AWS X-Ray y trazabilidad distribuida
- AWS CloudTrail
- AWS Config
- AWS Trusted Advisor
Módulo 6: Bases de datos
- Cómo elegir la base de datos adecuada
- Amazon DynamoDB
- Amazon Aurora
- Amazon Redshift
- Amazon ElastiCache
Módulo 7: Integración de aplicaciones
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Patrones de integración: idempotencia, reintentos y colas de mensajes fallidos
