La lección anterior dejó a MercadoFresco a medio desacoplar. cola-mercadofresco-pedidos funciona, la confirmación baja a 400 ms y el ERP puede caerse sin llevarse ventas por delante, pero la cola es un canal punto a punto: cada mensaje lo procesa un consumidor y desaparece. Con almacén, correo, reparto y analítica interesados en el mismo hecho, la tienda ha acabado enviando cuatro mensajes a cuatro colas distintas. Vuelve a saber quiénes son sus consumidores, que es exactamente lo que queríamos evitar. Y cuando márketing pida enterarse de los pedidos para su programa de fidelización, habrá que tocar el código de la tienda, desplegarlo y probarlo.

Amazon SNS (Simple Notification Service) es el servicio de publicación/suscripción de AWS. La tienda publica una vez en un tema —«se ha confirmado el pedido PED-084417»— y SNS entrega una copia a cada suscriptor. Quién esté suscrito es un detalle de configuración, no de código. Añadir a márketing pasa de ser un despliegue a ser un comando de una línea.

En esta lección Luis crea mercadofresco-pedido-confirmado, monta el patrón fan-out SNS→SQS con las colas de almacén, correo y analítica, filtra por atributos para que la cola del reparto en 24 horas solo reciba lo que le incumbe, y entiende por fin qué era realmente el tema alertas-mercadofresco que llevamos usando desde el módulo 5.

Aviso de coste. SNS cobra por publicación y por entrega: el primer millón de peticiones de la API es gratis, y las entregas a SQS y Lambda también lo son. Lo que cuesta de verdad es el SMS (unos 0,06 USD por mensaje en España) y, en menor medida, el correo. Un bucle de pruebas enviando SMS puede generar una factura sorprendente en minutos. Al final tienes la limpieza. Datos ficticios.

Contenido

  1. Publicación/suscripción frente a la cola punto a punto
  2. Temas, publicadores y suscriptores
  3. Tipos de suscripción y sus garantías
  4. Confirmación de suscripción y sus trampas
  5. El patrón fan-out SNS→SQS
  6. Por qué una cola entre medias y no una Lambda directa
  7. Montar el fan-out de MercadoFresco por CLI y con boto3
  8. Filtrado por atributos y por cuerpo del mensaje
  9. Reintentos, políticas de entrega y DLQ del propio SNS
  10. Mensajes con estructura por protocolo
  11. Temas FIFO y su encaje con colas FIFO
  12. Seguridad: política de tema, cifrado y acceso entre cuentas
  13. alertas-mercadofresco revisitado, SMS y correo transaccional
  14. Métricas y depuración de entregas
  15. Coste y limpieza
  16. Errores comunes y consejos
  17. Ejercicios
  18. Conclusión

Publicación/suscripción frente a la cola punto a punto

La diferencia entre SQS y SNS no es de tecnología, es de quién conoce a quién.

Cola (SQS) Tema (SNS)
Modelo Punto a punto Publicación/suscripción
Consumidores por mensaje Uno (el que lo coja) Todos los suscritos
Quién conoce a quién El productor conoce la cola El publicador no conoce a nadie
Almacenamiento Duradero hasta 14 días Ninguno: entrega y olvida
Si el destino está caído El mensaje espera Reintentos y luego se pierde
Modelo de consumo Sondeo (pull) Inserción (push)
Añadir un consumidor Requiere que el productor envíe también ahí Una suscripción, sin tocar el productor

La fila que hay que grabarse es la de almacenamiento: SNS no guarda nada. Un tema es un enrutador, no un buzón. Si publicas en un tema sin suscriptores, el mensaje se evapora sin error ni aviso. Y si el único suscriptor es un endpoint HTTP caído, SNS reintenta según su política de entrega y, agotada, descarta el mensaje. Esta es la razón número uno de que el patrón correcto para trabajo importante sea SNS más SQS, no SNS a secas.

flowchart LR
    subgraph antes["Antes: la tienda conoce a sus cuatro consumidores"]
        T1[Tienda] --> QA1[(cola almacen)]
        T1 --> QB1[(cola correo)]
        T1 --> QC1[(cola analitica)]
        T1 --> QD1[(cola reparto)]
    end
    subgraph despues["Despues: la tienda publica un hecho y se desentiende"]
        T2[Tienda] -->|1 Publish| TEMA{{mercadofresco-pedido-confirmado}}
        TEMA --> QA2[(cola-mercadofresco-almacen)]
        TEMA --> QB2[(cola-mercadofresco-correo)]
        TEMA --> QC2[(cola-mercadofresco-analitica)]
        TEMA -.->|nuevo, sin tocar la tienda| QE2[(cola-mercadofresco-fidelizacion)]
    end
    style TEMA fill:#cfe2ff
    style QE2 fill:#d4edda

El cambio conceptual es que la tienda deja de dar órdenes («mete esto en la cola del almacén») y pasa a comunicar hechos («se ha confirmado un pedido»). Un hecho no tiene destinatario: quien tenga interés se suscribe. Esa inversión —de comando a evento— es la base de la arquitectura dirigida por eventos que profundizaremos en 07-03.

Temas, publicadores y suscriptores

Un tema (topic) es un canal lógico con nombre y ARN. Crearlo es instantáneo y gratuito:

export PERFIL="--profile mercadofresco-dev --region eu-west-1"
export ETIQUETAS='Key=Proyecto,Value=mercadofresco Key=Entorno,Value=produccion Key=Componente,Value=integracion Key=Propietario,Value=marta Key=CentroCoste,Value=tecnologia'

aws sns create-topic --name mercadofresco-pedido-confirmado \
  --attributes KmsMasterKeyId=alias/mercadofresco-datos \
  --tags $ETIQUETAS $PERFIL
# → arn:aws:sns:eu-west-1:111122223333:mercadofresco-pedido-confirmado

Hay dos clases de tema:

Tema estándar Tema FIFO
Nombre Libre Debe acabar en .fifo
Orden No garantizado Estricto por MessageGroupId
Entrega Al menos una vez Exactamente una vez (ventana de 5 min)
Rendimiento Prácticamente ilimitado 300 publicaciones/s (3.000 en alto rendimiento)
Suscriptores permitidos Todos Solo colas SQS FIFO (y Lambda desde 2023)
Precio de publicación 0,50 USD/millón 0,30 USD/millón + 0,017 USD/GB

Un publicador llama a Publish con el ARN del tema; solo necesita sns:Publish. Un suscriptor es un endpoint registrado en el tema mediante Subscribe, con un protocolo y una dirección. El límite por tema es de 12,5 millones de suscripciones, así que en la práctica no existe.

Tipos de suscripción y sus garantías

Protocolo Endpoint Reintentos Caso de uso en MercadoFresco
sqs ARN de cola Hasta 100.000 s (~23 h) El principal: trabajo asíncrono fiable
lambda ARN de función Hasta 100.000 s Reacción trivial sin necesidad de amortiguar
https / http URL Configurable, hasta 23 h con backoff Webhook al ERP del socio
email / email-json Dirección Sin reintentos útiles Avisos a Marta desde alertas-mercadofresco
sms Número E.164 Depende de la operadora Aviso al repartidor de urgencia
application Endpoint de plataforma móvil Notificación push a la app de reparto
firehose ARN de flujo de entrega Volcado de eventos a mercadofresco-registros-web

Las garantías no son iguales. Las entregas a destinos internos de AWS (SQS, Lambda, Firehose) son las más fiables: reintentos largos, sin dependencia de red pública y DLQ disponible. Las entregas HTTP/S dependen de que tu endpoint esté vivo, responda un 2xx en menos de 15 segundos y aguante ráfagas. Las de correo y SMS son, a efectos prácticos, «a lo sumo una vez»: si el proveedor rechaza, SNS no puede hacer gran cosa, y no son adecuadas para nada que deba ocurrir sí o sí.

Confirmación de suscripción y sus trampas

Los protocolos que apuntan fuera de AWS —HTTP/S y correo— requieren confirmación. SNS envía al endpoint un mensaje de tipo SubscriptionConfirmation con un SubscribeURL, y hasta que alguien lo visita la suscripción queda en PendingConfirmation y no recibe nada.

{
  "Type": "SubscriptionConfirmation",
  "MessageId": "8f21a3c4-...",
  "Token": "2336412f37...",
  "TopicArn": "arn:aws:sns:eu-west-1:111122223333:mercadofresco-pedido-confirmado",
  "Message": "You have chosen to subscribe to the topic ...",
  "SubscribeURL": "https://sns.eu-west-1.amazonaws.com/?Action=ConfirmSubscription&...",
  "Timestamp": "2026-08-02T18:41:07.000Z",
  "SignatureVersion": "1",
  "Signature": "EXAMPLEpH+..."
}

Las trampas, todas vistas en producción:

  • El endpoint HTTP recibe SubscriptionConfirmation y no sabe qué hacer con él. Tu manejador espera Type: "Notification" y devuelve 400. La suscripción nunca se confirma y nadie se entera hasta que falta un evento. El manejador debe distinguir los tres tipos: SubscriptionConfirmation, Notification y UnsubscribeConfirmation.
  • Confirmar sin verificar la firma. Cualquiera que conozca tu URL puede enviarte un JSON con un SubscribeURL apuntando a su propio tema y, si lo visitas a ciegas, acabas suscrito a un tema ajeno. Verifica siempre Signature con el certificado de SigningCertURL —y comprueba que ese dominio es sns.<region>.amazonaws.com—.
  • El correo de confirmación acaba en spam. El aviso de SNS llega desde [email protected]; los filtros corporativos lo bloquean con frecuencia.
  • El token caduca a los 3 días. Pasado ese plazo hay que volver a suscribir.
  • AuthenticateOnUnsubscribe. Sin este atributo, cualquiera con el enlace de baja puede desuscribir el endpoint. Ponlo a true en suscripciones HTTP de producción.

Las suscripciones a SQS, Lambda y Firehose no requieren confirmación cuando las crea un principal de la misma cuenta con permisos suficientes: se confirman solas. Es una razón más para preferirlas.

El patrón fan-out SNS→SQS

Este es el patrón de integración más usado en AWS y el que MercadoFresco adopta como estándar: un tema al que se suscriben varias colas, y un consumidor por cola.

flowchart TD
    T[Tienda: Publish] --> TEMA{{mercadofresco-pedido-confirmado}}
    TEMA -->|sin filtro| QA[(cola-mercadofresco-almacen)]
    TEMA -->|sin filtro| QB[(cola-mercadofresco-correo)]
    TEMA -->|sin filtro| QC[(cola-mercadofresco-analitica)]
    TEMA -->|filtro franja_entrega = 24h| QD[(cola-mercadofresco-reparto-24h)]
    QA --> CA[Consumidor ERP] --> DA[/ERP del almacen/]
    QB --> CB[Lambda correo] --> DB[/Proveedor SMTP/]
    QC --> CC[Consumidor analitica] --> DC[(Redshift)]
    QD --> CD[Lambda reparto] --> DD[/API del repartidor/]
    QA -.->|4 fallos| DLQA[(mercadofresco-almacen-fallidas)]
    QB -.->|4 fallos| DLQB[(mercadofresco-correo-fallidas)]
    style TEMA fill:#cfe2ff
    style DLQA fill:#f8d7da
    style DLQB fill:#f8d7da

Cada consumidor avanza a su ritmo. El del ERP puede ir lento y acumular 4.000 mensajes sin que el correo se entere. Si el consumidor de analítica tiene un fallo y hay que pararlo dos horas, sus mensajes esperan en su cola y se procesan después: nadie más se ve afectado.

Por qué una cola entre medias y no una Lambda directa

SNS puede invocar funciones Lambda directamente, y es tentador ahorrarse la cola. Es un error en cuanto el trabajo importa. Tres razones:

1. Reintentos y durabilidad. Si SNS invoca una Lambda y la función falla, SNS reintenta según su política de entrega y luego descarta el mensaje (o lo manda a su DLQ, si la configuraste). Con una cola de por medio, el mensaje está guardado hasta 14 días, el consumidor lo reintenta las veces que digas y acaba en una DLQ que puedes inspeccionar y reprocesar con start-message-move-task. La cola convierte un evento efímero en trabajo pendiente persistente.

2. Amortiguación y contrapresión. El viernes a las 18:00 llegan 900 pedidos/hora en ráfagas. Con SNS→Lambda, Lambda escala tan rápido como lleguen las invocaciones: si detrás está Aurora con 200 conexiones o el SMTP con su límite de envío, ese pico se propaga intacto. Con SNS→SQS→Lambda, la cola absorbe la ráfaga y MaximumConcurrency (07-01) marca a qué ritmo se drena.

3. Aislamiento de fallos. Con cuatro Lambdas suscritas al tema y una de ellas estrangulada por el límite de concurrencia de la cuenta, sus invocaciones fallan y se pierden. Con cuatro colas, el problema queda contenido en la cola afectada.

Criterio SNS → Lambda SNS → SQS → consumidor
Durabilidad del trabajo Nula tras agotar reintentos Hasta 14 días
Reprocesar tras arreglar el fallo Imposible Redrive desde la DLQ
Control del ritmo Ninguno MaximumConcurrency, tamaño de lote
Aislamiento entre consumidores Bajo Alto
Latencia añadida ~0 Decenas de ms
Piezas que mantener 1 2

Cuándo sí conviene SNS→Lambda directo: reacciones triviales, idempotentes y sin consecuencias si se pierde alguna —refrescar una caché, escribir una métrica—, y cuando la latencia mínima es un requisito. Para todo lo que sea una venta, un cobro o un aviso a un socio, cola de por medio.

Montar el fan-out de MercadoFresco por CLI y con boto3

El paso que más se olvida es la política de cola: SNS no es un principal IAM de tu cuenta, así que necesita permiso explícito en cada cola de destino.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "PermitirEntregaDesdeTemaPedidoConfirmado",
    "Effect": "Allow",
    "Principal": { "Service": "sns.amazonaws.com" },
    "Action": "sqs:SendMessage",
    "Resource": "arn:aws:sqs:eu-west-1:111122223333:cola-mercadofresco-almacen",
    "Condition": {
      "ArnEquals": {
        "aws:SourceArn": "arn:aws:sns:eu-west-1:111122223333:mercadofresco-pedido-confirmado"
      }
    }
  }]
}

Sin la condición aws:SourceArn, cualquier tema de SNS del mundo podría escribir en tu cola. Es el mismo patrón de protección que en 07-01 con S3.

Con eso, el montaje completo:

TEMA=arn:aws:sns:eu-west-1:111122223333:mercadofresco-pedido-confirmado

for C in almacen correo analitica; do
  COLA_ARN=arn:aws:sqs:eu-west-1:111122223333:cola-mercadofresco-$C
  aws sqs set-queue-attributes \
    --queue-url https://sqs.eu-west-1.amazonaws.com/111122223333/cola-mercadofresco-$C \
    --attributes Policy="$(sed "s|COLA_ARN|$COLA_ARN|" politica-cola.json | tr -d '\n')" $PERFIL

  aws sns subscribe --topic-arn $TEMA --protocol sqs --notification-endpoint $COLA_ARN \
    --attributes RawMessageDelivery=true $PERFIL
done

RawMessageDelivery=true merece un párrafo propio. Por defecto, SNS envuelve tu mensaje en un sobre JSON con metadatos, y el cuerpo original queda como una cadena escapada dentro del campo Message:

{
  "Type": "Notification",
  "MessageId": "1d9a...",
  "TopicArn": "arn:aws:sns:eu-west-1:111122223333:mercadofresco-pedido-confirmado",
  "Message": "{\"pedido_id\":\"PED-2026-084417\",\"importe_eur\":48.20}",
  "Timestamp": "2026-08-02T18:41:07.123Z",
  "MessageAttributes": { "franja_entrega": { "Type": "String", "Value": "24h" } }
}

Esto obliga al consumidor a hacer un doble json.loads y a leer los atributos de un sitio distinto al que los leería si el mensaje llegara directo a la cola. Con RawMessageDelivery=true, la cola recibe el cuerpo tal cual y los atributos de SNS se convierten en atributos nativos de SQS: el mismo consumidor funciona tanto si el mensaje viene del tema como si se envió directamente a la cola. Actívalo siempre en suscripciones SQS y Firehose.

El publicador queda así:

import json, os
from datetime import datetime, timezone
import boto3

sns = boto3.client("sns", region_name="eu-west-1")
TEMA = os.environ["ARN_TEMA_PEDIDO_CONFIRMADO"]


def publicar_pedido_confirmado(pedido):
    """La tienda comunica un hecho. No sabe ni le importa quién se entera."""
    respuesta = sns.publish(
        TopicArn=TEMA,
        Subject=f"Pedido confirmado {pedido['pedido_id']}",   # solo lo usan correo y HTTP
        Message=json.dumps({
            "version": 1,
            "pedido_id": pedido["pedido_id"],
            "cliente_id": pedido["cliente_id"],
            "importe_eur": float(pedido["importe_eur"]),
            "franja_entrega": pedido["franja_entrega"],       # "24h" | "estandar"
            "provincia": pedido["provincia"],
            "lineas": pedido["lineas"],
            "confirmado_en": datetime.now(timezone.utc).isoformat(),
        }, ensure_ascii=False),
        MessageAttributes={
            # Estos atributos son los que evalúan las políticas de filtro.
            "tipo_evento":    {"DataType": "String", "StringValue": "PedidoConfirmado"},
            "franja_entrega": {"DataType": "String", "StringValue": pedido["franja_entrega"]},
            "provincia":      {"DataType": "String", "StringValue": pedido["provincia"]},
            "importe_eur":    {"DataType": "Number", "StringValue": str(pedido["importe_eur"])},
        },
    )
    return respuesta["MessageId"]

Fíjate en que los campos que sirven para filtrar están duplicados: en el cuerpo, porque el consumidor los necesita, y en los atributos, porque SNS solo mira ahí por defecto. Es una duplicación deliberada y barata.

Para volúmenes altos, publish_batch acepta 10 mensajes por llamada con la misma trampa que send_message_batch: devuelve 200 con una lista Failed que hay que inspeccionar.

Filtrado por atributos y por cuerpo del mensaje

Una política de filtro es un JSON asociado a la suscripción. Si el mensaje no encaja, SNS no lo entrega a ese suscriptor —y no lo cobra—. Es la pieza que evita el antipatrón de «todos reciben todo y cada consumidor descarta lo que no le interesa», que desperdicia invocaciones y llena las colas.

Marta quiere que cola-mercadofresco-reparto-24h solo reciba pedidos de la franja de 24 horas:

{ "franja_entrega": ["24h"] }
aws sns set-subscription-attributes \
  --subscription-arn arn:aws:sns:eu-west-1:111122223333:mercadofresco-pedido-confirmado:9c1f... \
  --attribute-name FilterPolicy \
  --attribute-value '{"franja_entrega":["24h"]}' $PERFIL

El lenguaje de filtros admite mucho más que la igualdad:

Operador Ejemplo Significado
Coincidencia exacta {"franja_entrega": ["24h", "express"]} Vale cualquiera de los dos
anything-but {"provincia": [{"anything-but": ["Baleares", "Canarias"]}]} Todo menos península insular
prefix {"tipo_evento": [{"prefix": "Pedido"}]} PedidoConfirmado, PedidoCancelado
suffix {"sku": [{"suffix": "-BIO"}]} Productos ecológicos
numeric {"importe_eur": [{"numeric": [">=", 150]}]} Pedidos grandes
Rango numérico {"importe_eur": [{"numeric": [">", 50, "<=", 200]}]} Entre 50 y 200 €
exists {"cupon": [{"exists": true}]} Solo si el atributo está presente
Combinación OR {"franja_entrega": ["24h"], "importe_eur": [{"numeric": [">=", 150]}]} Ver abajo

La regla que más confunde: dentro de un atributo, la lista es un OR; entre atributos distintos, es un AND. La última fila de la tabla exige franja 24h y además importe ≥ 150. Para expresar un OR entre atributos distintos hay que usar $or:

{
  "$or": [
    { "franja_entrega": ["24h"] },
    { "importe_eur": [{ "numeric": [">=", 150] }] }
  ]
}

Filtrado por cuerpo del mensaje. Desde 2022 SNS puede filtrar mirando dentro del JSON del cuerpo, lo que elimina la duplicación de campos. Se activa poniendo FilterPolicyScope a MessageBody:

aws sns set-subscription-attributes --subscription-arn $SUB \
  --attribute-name FilterPolicyScope --attribute-value MessageBody $PERFIL

aws sns set-subscription-attributes --subscription-arn $SUB \
  --attribute-name FilterPolicy \
  --attribute-value '{"franja_entrega":["24h"],"lineas":{"sku":[{"prefix":"PESC-"}]}}' $PERFIL

La política puede navegar objetos anidados y arrays. Dos límites que importan: el cuerpo debe ser JSON válido —si no, la suscripción no recibe nada y el fallo es silencioso— y una suscripción tiene un único ámbito de filtro, o atributos o cuerpo, nunca los dos.

Filtro por atributos Filtro por cuerpo
Duplicar campos No
Anidamiento y arrays No
Si el cuerpo no es JSON Funciona igual No entrega nada
Coste de publicación Igual Igual
Recomendación Contratos estables y campos planos Filtros ricos sobre el detalle

MercadoFresco usa atributos para el enrutamiento básico (tipo de evento, franja, provincia) y cuerpo para casos ricos, como avisar al equipo de frío cuando alguna línea empieza por PESC-.

Reintentos, políticas de entrega y DLQ del propio SNS

Cuando SNS no consigue entregar, reintenta según la política de entrega del protocolo. Para SQS, Lambda y Firehose está predefinida y es generosa: fases inmediata, con pre-backoff, con backoff exponencial y post-backoff, hasta unas 23 horas en total. Para HTTP/S es configurable:

{
  "healthyRetryPolicy": {
    "minDelayTarget": 5,
    "maxDelayTarget": 300,
    "numRetries": 50,
    "numNoDelayRetries": 0,
    "numMinDelayRetries": 3,
    "numMaxDelayRetries": 10,
    "backoffFunction": "exponential"
  },
  "throttlePolicy": { "maxReceivesPerSecond": 20 }
}

maxReceivesPerSecond es la contrapresión hacia el ERP del socio: aunque MercadoFresco publique 900 mensajes en un minuto, SNS no le enviará más de 20 por segundo. Es la única forma de proteger a un endpoint HTTP que no puedes escalar.

La DLQ de SNS se configura por suscripción, no por tema, con el atributo RedrivePolicy. Recoge los mensajes que agotaron los reintentos hacia ese suscriptor concreto:

aws sns set-subscription-attributes --subscription-arn $SUB_ERP \
  --attribute-name RedrivePolicy \
  --attribute-value '{"deadLetterTargetArn":"arn:aws:sqs:eu-west-1:111122223333:mercadofresco-sns-fallidas"}' \
  $PERFIL

Hay que distinguir bien dos DLQ que conviven en la misma arquitectura y no significan lo mismo:

DLQ de la suscripción SNS DLQ de la cola SQS
Qué recoge Lo que SNS no pudo entregar Lo que el consumidor no pudo procesar
Causa típica Endpoint caído, permisos, cola borrada Datos inválidos, dependencia caída
Se configura en La suscripción La cola de origen
Señal Problema de infraestructura o permisos Problema de aplicación o de datos

Ambas necesitan alarma sobre ApproximateNumberOfMessagesVisible hacia alertas-mercadofresco.

Mensajes con estructura por protocolo

Un mismo evento no se lee igual en un SMS de 160 caracteres que en una cola. Con MessageStructure="json", un solo Publish lleva una carga distinta por protocolo:

sns.publish(
    TopicArn=TEMA_ALERTAS,
    Subject="Cola de pedidos retrasada",
    MessageStructure="json",
    Message=json.dumps({
        "default": "La cola cola-mercadofresco-pedidos supera los 5 minutos de antiguedad.",
        "email": ("Alarma: mercadofresco-pedidos-cola-retrasada\n\n"
                  "ApproximateAgeOfOldestMessage > 300 s durante 3 periodos.\n"
                  "Panel: https://console.aws.amazon.com/cloudwatch/home#dashboards:name=mercadofresco-produccion"),
        "sms": "MercadoFresco: cola de pedidos retrasada >5 min",
        "sqs": json.dumps({"alarma": "cola-retrasada", "cola": "cola-mercadofresco-pedidos"}),
    }),
)

La clave default es obligatoria y se usa para cualquier protocolo sin entrada propia. Si falta, Publish devuelve InvalidParameter. El Subject solo lo usan correo y HTTP; se ignora en SQS y Lambda, y está limitado a 100 caracteres ASCII.

Sobre los atributos: un mensaje admite hasta 10, y su tamaño cuenta para el límite de 256 KiB del mensaje. Con MessageStructure="json", cada carga por protocolo también cuenta contra ese límite total.

Temas FIFO y su encaje con colas FIFO

Un tema FIFO garantiza orden y deduplicación de extremo a extremo, pero solo puede entregar a colas SQS FIFO (y, desde 2023, a funciones Lambda). No admite HTTP, correo ni SMS. Encaja con la cola de stock que creamos en 07-01:

aws sns create-topic --name mercadofresco-stock-movimientos.fifo \
  --attributes FifoTopic=true,ContentBasedDeduplication=false \
  --tags $ETIQUETAS $PERFIL

aws sns subscribe --topic-arn arn:aws:sns:eu-west-1:111122223333:mercadofresco-stock-movimientos.fifo \
  --protocol sqs \
  --notification-endpoint arn:aws:sqs:eu-west-1:111122223333:cola-mercadofresco-stock.fifo \
  --attributes RawMessageDelivery=true $PERFIL

Al publicar hay que aportar MessageGroupId y, si no hay deduplicación por contenido, MessageDeduplicationId:

sns.publish(
    TopicArn=TEMA_STOCK,
    Message=json.dumps(movimiento, ensure_ascii=False),
    MessageGroupId=movimiento["sku"],                       # orden por producto
    MessageDeduplicationId=f"{movimiento['pedido_id']}:{movimiento['sku']}:{movimiento['motivo']}",
    MessageAttributes={"motivo": {"DataType": "String", "StringValue": movimiento["motivo"]}},
)

El MessageGroupId se propaga a la cola, de modo que el orden se conserva a lo largo de toda la cadena. Y el filtrado por atributos también funciona en temas FIFO, lo que permite que una cola reciba solo las reservas y otra todos los movimientos.

Seguridad: política de tema, cifrado y acceso entre cuentas

La política de tema controla quién publica y quién se suscribe. Por defecto, solo el propietario de la cuenta. Esta la endurece: la tienda publica, y solo se pueden crear suscripciones SQS.

{
  "Version": "2012-10-17",
  "Id": "politica-mercadofresco-pedido-confirmado",
  "Statement": [
    {
      "Sid": "SoloLaTiendaPublica",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:role/rol-mercadofresco-tienda" },
      "Action": "sns:Publish",
      "Resource": "arn:aws:sns:eu-west-1:111122223333:mercadofresco-pedido-confirmado"
    },
    {
      "Sid": "SuscripcionesSoloDeColas",
      "Effect": "Allow",
      "Principal": { "AWS": "111122223333" },
      "Action": "sns:Subscribe",
      "Resource": "arn:aws:sns:eu-west-1:111122223333:mercadofresco-pedido-confirmado",
      "Condition": { "StringEquals": { "sns:Protocol": "sqs" } }
    }
  ]
}

La condición sns:Protocol es una salvaguarda práctica: impide que alguien suscriba su correo personal a un tema que transporta direcciones de clientes. También existe sns:Endpoint para restringir a endpoints concretos.

Cifrado. KmsMasterKeyId=alias/mercadofresco-datos cifra los mensajes en reposo dentro de SNS. Dos advertencias: el publicador necesita kms:GenerateDataKey* y kms:Decrypt sobre la clave, y si un servicio de AWS publica en el tema —CloudWatch al disparar una alarma, S3 al subirse un objeto— la política de la clave debe permitírselo explícitamente. Es la causa habitual de alarmas que dejan de avisar justo después de activar el cifrado.

Acceso entre cuentas. El socio de reparto tiene su propia cuenta AWS y quiere consumir los eventos. No se le dan credenciales: se añade su cuenta a la política del tema con sns:Subscribe, y él crea en su cuenta una cola cuya política acepta a nuestro tema. Ninguna de las dos partes obtiene acceso a la infraestructura de la otra. Es el mismo principio de menor privilegio de 04-01 aplicado a la integración.

alertas-mercadofresco revisitado, SMS y correo transaccional

Desde el módulo 5 llevamos escribiendo --alarm-actions arn:aws:sns:...:alertas-mercadofresco sin explicar del todo qué ocurría. Ahora está claro: una alarma de CloudWatch es un publicador de SNS. Cuando pasa a ALARM, publica un JSON con AlarmName, NewStateValue, NewStateReason y las dimensiones de la métrica. El tema tiene suscrito el correo de Marta, y podría tener suscritos —sin tocar ni una alarma— una cola que registre el historial, una Lambda que abra un ticket o un endpoint HTTPS hacia la herramienta de guardias.

Ese es exactamente el valor del modelo: CloudWatch no sabe quién se entera de sus alarmas. Añadir un destino no requiere modificar 40 alarmas, solo crear una suscripción.

Una mejora inmediata ahora que conocemos el filtrado: suscribir el SMS de guardia con una política que solo deje pasar lo crítico.

{ "AlarmName": [{ "prefix": "mercadofresco-pedidos-" }], "NewStateValue": ["ALARM"] }

Con FilterPolicyScope=MessageBody, Marta recibe SMS solo por alarmas de pedidos que entran en estado ALARM, mientras el correo sigue recibiéndolo todo, incluidas las vueltas a OK.

Sobre SMS y correo. SNS sirve para avisos operativos a personas conocidas: un SMS al técnico de guardia, un correo a Marta. No sirve para correo transaccional ni para campañas: no hay plantillas, ni seguimiento de aperturas, ni gestión de rebotes, ni control de reputación del remitente, y el remitente es [email protected]. El correo de confirmación de pedido de MercadoFresco se envía con Amazon SES, que sí ofrece dominio propio, DKIM, plantillas y métricas de entregabilidad; la Lambda suscrita a cola-mercadofresco-correo llama a SES, no a SNS. Para SMS masivos, SNS exige además salir del entorno aislado (sandbox) y registrar el remitente ante la operadora.

Métricas y depuración de entregas

Métrica Qué indica Acción
NumberOfMessagesPublished Volumen publicado Referencia de tráfico
NumberOfNotificationsDelivered Entregas correctas Comparar con lo publicado × suscriptores
NumberOfNotificationsFailed Entregas fallidas Alarma inmediata
NumberOfNotificationsFilteredOut Descartadas por política de filtro Si es 100 %, el filtro está mal
NumberOfNotificationsFilteredOut-NoMessageAttributes El mensaje no traía atributos Publicador que olvidó los atributos
NumberOfNotificationsFilteredOut-InvalidAttributes Atributos con tipo incorrecto Números enviados como cadena, etc.
PublishSize Tamaño de los mensajes Cerca de 256 KiB → usar S3
SMSMonthToDateSpentUSD Gasto en SMS del mes Alarma de coste

Las tres métricas de FilteredOut son las que más tiempo ahorran. El síntoma clásico —«la cola no recibe nada y no hay ningún error»— casi siempre es un filtro: o el publicador no envía los atributos, o los envía con el tipo equivocado. Un Number enviado como String no encaja con un filtro numeric, y SNS lo descarta sin decir nada.

aws cloudwatch put-metric-alarm --alarm-name mercadofresco-sns-entregas-fallidas \
  --namespace AWS/SNS --metric-name NumberOfNotificationsFailed \
  --dimensions Name=TopicName,Value=mercadofresco-pedido-confirmado \
  --statistic Sum --period 300 --evaluation-periods 1 --threshold 0 \
  --comparison-operator GreaterThanThreshold --treat-missing-data notBreaching \
  --alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco $PERFIL

Para depurar entregas fallidas hace falta activar los registros de estado de entrega, que no están activos por defecto: se configura un rol con permiso de escritura en CloudWatch Logs y un porcentaje de muestreo (SQSSuccessFeedbackSampleRate, HTTPSuccessFeedbackSampleRate…). Con ellos aparecen en Logs los códigos de respuesta reales del endpoint, que es lo único que permite distinguir un 403 por permisos de un 500 del socio. Y como todo Publish queda en trail-mercadofresco (05-03), siempre se puede comprobar si el evento llegó a publicarse.

Coste y limpieza

Concepto Precio (eu-west-1) MercadoFresco
Publicaciones 1 M gratis, luego 0,50 USD/M 240.000/mes → 0 USD
Entregas a SQS y a Lambda Gratis 960.000 → 0 USD
Entregas HTTP/S 0,60 USD/M 240.000 → 0,14 USD
Entregas por correo 2,00 USD/100.000 300/mes → 0,01 USD
SMS a España ~0,06 USD/mensaje 40/mes → 2,40 USD
Datos transferidos 0,09 USD/GB salida ~0,4 GB → 0,04 USD
Total ~2,60 USD/mes

Lo que hay que vigilar es el SMS: un bucle de pruebas que envíe 5.000 mensajes cuesta 300 USD en unos minutos. Pon siempre MonthlySpendLimit en las preferencias de SMS de la cuenta y una alarma sobre SMSMonthToDateSpentUSD. Nótese además que el fan-out multiplica las entregas: publicar 240.000 veces con cuatro suscriptores son 960.000 entregas, gratuitas hacia SQS pero de pago hacia HTTP.

aws sns list-subscriptions-by-topic --topic-arn $TEMA \
  --query 'Subscriptions[].SubscriptionArn' --output text $PERFIL | \
  xargs -n1 -I{} aws sns unsubscribe --subscription-arn {} $PERFIL
aws sns delete-topic --topic-arn $TEMA $PERFIL
aws sns delete-topic --topic-arn arn:aws:sns:eu-west-1:111122223333:mercadofresco-stock-movimientos.fifo $PERFIL

Borrar el tema no borra las colas suscritas ni sus mensajes: hay que limpiarlas aparte con los comandos de 07-01. Y ojo, borrar un tema no elimina las suscripciones por correo pendientes de confirmar.

Errores Comunes y Consejos

Publicar en un tema sin suscriptores. Publish devuelve 200 y un MessageId, y el mensaje se evapora. No hay error, no hay métrica de fallo. Verifica NumberOfNotificationsDelivered tras cualquier despliegue que toque suscripciones.

Olvidar la política de cola. La suscripción se crea sin quejarse, pero SNS no puede escribir. Los mensajes se pierden y NumberOfNotificationsFailed sube. Es el fallo número uno del fan-out.

Política de cola sin aws:SourceArn. Funciona, pero deja tu cola abierta a cualquier tema de SNS.

No activar RawMessageDelivery. El consumidor recibe el sobre de SNS, necesita doble json.loads y los atributos aparecen donde no los busca. Actívalo siempre en suscripciones SQS.

Filtros que descartan todo en silencio. El publicador no envía los atributos, o envía un número como String cuando el filtro usa numeric. Síntoma: cero mensajes y cero errores. Mira NumberOfNotificationsFilteredOut y sus dos variantes. Y no confundas OR y AND: dentro de un atributo, lista = OR; entre atributos distintos, AND; para OR entre atributos hace falta $or.

Suscribir Lambdas directamente para trabajo importante, o suponer que SNS guarda los mensajes. No los guarda: un consumidor caído durante el despliegue pierde definitivamente todo lo publicado en esa ventana si no hay cola de por medio.

Cifrar el tema y olvidar la política de la clave. Las alarmas de CloudWatch dejan de publicar y nadie se entera —porque justamente el mecanismo de aviso es el que ha fallado—.

Consejo: nombra los temas por el hecho, no por el destino. mercadofresco-pedido-confirmado, no mercadofresco-avisar-almacen. El nombre es parte del contrato y no debe envejecer cuando cambien los consumidores.

Consejo: pon version en el cuerpo desde el primer mensaje. Cuando dentro de un año haya que añadir un campo, los consumidores antiguos podrán ignorarlo con seguridad.

Consejo: filtra en el tema, no en el consumidor. Un consumidor que descarta el 90 % de lo que recibe paga invocaciones, peticiones a la cola y complejidad por nada.

Ejercicios

Ejercicio 1: el consumidor de márketing

Márketing quiere procesar los pedidos confirmados para su programa de fidelización, pero solo le interesan los de importe igual o superior a 60 € o los de clientes de Cataluña, y su sistema es un endpoint HTTPS que aguanta 5 peticiones por segundo y sufre caídas de hasta 40 minutos.

Diseña la integración: (a) qué suscribes al tema y por qué; (b) la política de filtro exacta en JSON; (c) qué configuras para proteger su endpoint del pico del viernes; (d) qué DLQ intervienen y qué significa cada una; (e) qué cambia en el código de la tienda.

Ejercicio 2: diagnóstico del fan-out mudo

Luis despliega el fan-out un jueves. El viernes por la mañana, Marta observa: NumberOfMessagesPublished 8.400; NumberOfNotificationsDelivered 16.800; NumberOfNotificationsFailed 8.400; NumberOfNotificationsFilteredOut 0. La cola del almacén y la de correo reciben con normalidad; la de analítica está vacía. cola-mercadofresco-analitica existe, su consumidor funciona si se le envían mensajes a mano, y la suscripción aparece como Confirmed.

Explica: (a) qué falla exactamente y cómo lo deduces de los números; (b) por qué la suscripción está confirmada aunque no funcione; (c) qué comando ejecutarías para confirmar el diagnóstico; (d) la corrección; (e) qué alarma habría avisado el jueves por la tarde.

Ejercicio 3: SNS o SQS

Para cada caso, decide si usar SNS, SQS o SNS→SQS, y justifica en dos frases: (a) la tienda avisa al almacén de que prepare una caja; (b) tres equipos quieren enterarse de las cancelaciones de pedido y se prevé un cuarto; (c) al subir una foto hay que generar la miniatura; (d) hay que avisar a Marta por SMS cuando la DLQ tenga mensajes; (e) hay que enviar los movimientos de stock manteniendo el orden por SKU a dos sistemas distintos; (f) hay que llamar al servicio de cálculo de portes para mostrar el precio en la pantalla de pago.

Soluciones

Solución 1

(a) Se suscribe una cola SQS (cola-mercadofresco-fidelizacion), no el endpoint HTTPS. El motivo es la caída de 40 minutos: si SNS entregara directamente por HTTP, dependeríamos de que la política de entrega aguante, y todo lo que fallara tras agotar reintentos se perdería. Con una cola, los eventos esperan hasta 14 días y se procesan cuando el sistema vuelva. Del consumo se encarga una Lambda suscrita a la cola que llama al endpoint de márketing.

(b) Como el requisito es un OR entre dos atributos distintos, hace falta $or:

{
  "$or": [
    { "importe_eur": [{ "numeric": [">=", 60] }] },
    { "provincia": ["Barcelona", "Girona", "Lleida", "Tarragona"] }
  ]
}

Si se escribieran los dos atributos en el mismo objeto sin $or, SNS exigiría ambas condiciones a la vez y márketing perdería la mayoría de los eventos. Requisito: la tienda debe publicar importe_eur como Number y provincia como String.

(c) La protección está en el consumidor, no en SNS: MaximumConcurrency=5 en el mapeo de origen de eventos de la Lambda, que limita el ritmo a lo que el endpoint aguanta; batch-size pequeño; y VisibilityTimeout de la cola ≥ 6 × timeout de la función. La cola absorbe el pico del viernes y lo drena a 5 peticiones por segundo. (Si en lugar de cola se hubiera suscrito el endpoint directamente, la herramienta equivalente sería throttlePolicy.maxReceivesPerSecond en la política de entrega.)

(d) Dos DLQ distintas. La DLQ de la suscripción SNS recogería fallos de entrega del tema a la cola —permisos mal puestos, cola borrada—; en la práctica casi nunca se activa porque la entrega a SQS es muy fiable, pero conviene tenerla. La DLQ de la cola (mercadofresco-fidelizacion-fallidas, con maxReceiveCount=4) recoge lo que la Lambda no pudo procesar: si tras 40 minutos de caída el endpoint sigue rechazando, esos eventos se aíslan ahí y se reprocesan con redrive.

(e) Nada. Ese es el resultado que buscábamos: la tienda ya publica el hecho con todos los atributos necesarios, y sumar a márketing es crear una cola, una política de cola, una suscripción con su filtro y una Lambda. Cero despliegues del código de la tienda.

Solución 2

(a) Los números cuadran exactamente con una de las tres suscripciones fallando siempre. Con 8.400 publicaciones y tres suscriptores deberían verse 25.200 entregas; hay 16.800 correctas (dos suscriptores × 8.400) y 8.400 fallidas (el tercero × 8.400). Como FilteredOut es 0, no es un problema de filtro: el mensaje se intenta entregar y la entrega falla. Combinado con que la cola de analítica existe y su consumidor funciona, el diagnóstico es claro: la política de cola-mercadofresco-analitica no permite a SNS escribir en ella. Casi con seguridad, el bucle de despliegue de Luis falló al aplicar la política en esa cola, o la aplicó con un aws:SourceArn equivocado.

(b) Confirmada y funcional son cosas distintas. Las suscripciones SQS dentro de la misma cuenta se autoconfirman al crearse: SNS comprueba que el ARN es válido, no que tenga permiso de escritura. El permiso se evalúa en cada entrega, no al suscribir. Por eso el estado es Confirmed y cada publicación falla con AccessDenied.

(c) aws sqs get-queue-attributes --queue-url ...cola-mercadofresco-analitica --attribute-names Policy para ver si hay política y si el aws:SourceArn coincide con el ARN del tema. Complementario: activar los registros de estado de entrega del tema y mirar en CloudWatch Logs el código de error real de las entregas fallidas, que dirá AccessDenied sin ambigüedad.

(d) Aplicar a la cola la misma política que a las otras dos, con Principal sns.amazonaws.com, acción sqs:SendMessage, Resource el ARN de la cola de analítica y condición ArnEquals sobre aws:SourceArn con el ARN del tema. Nada más: los mensajes de las últimas horas ya se han perdido —SNS no guarda—, y ahí se ve por qué el archivo y la reproducción de eventos de EventBridge (07-03) resultan tan valiosos.

(e) Una alarma sobre NumberOfNotificationsFailed > 0 del tema, con periodo de 5 minutos, habría avisado a los pocos minutos del despliegue del jueves en lugar de descubrirse el viernes por la mañana. Es la alarma mínima obligatoria de cualquier tema de SNS. Como complemento, ApproximateNumberOfMessagesVisible de la cola de analítica en 0 durante horas laborables también habría sido una señal.

Solución 3

(a) SQS. Es una orden con un único destinatario conocido y necesita durabilidad y reintentos. Un tema no aporta nada si solo hay un interesado, y añadiría una pieza más que mantener.

(b) SNS→SQS. Es el caso canónico de fan-out: varios interesados en el mismo hecho y uno más previsto. Cada equipo con su cola para aislarse de los fallos y del ritmo de los demás.

(c) SQS. Un solo consumidor y trabajo idempotente. S3 puede publicar directamente en la cola de miniaturas; meter un tema en medio solo tendría sentido si otro sistema necesitara enterarse de las fotos nuevas.

(d) SNS. El destino es una persona por un canal que SNS soporta nativamente. Aquí no hace falta cola: la alarma de CloudWatch publica y SNS entrega. Añadir SQS no mejoraría nada porque un SMS no se reprocesa.

(e) Tema FIFO → colas FIFO. Dos destinos exigen fan-out y el orden por SKU exige FIFO de extremo a extremo. mercadofresco-stock-movimientos.fifo con MessageGroupId igual al sku y dos colas FIFO suscritas, cada una con su filtro si procede.

(f) Ninguno: llamada síncrona. El cliente necesita el precio del porte ahora para poder decidir. Ni cola ni tema: una llamada HTTP con tiempo de espera corto, reintentos acotados y un valor por defecto si el servicio no responde. Es el recordatorio de que lo asíncrono no es siempre mejor —solo lo es cuando el resultado no forma parte de la respuesta al usuario—.

Conclusión

MercadoFresco ha pasado de una tienda que enviaba cuatro mensajes a cuatro colas a una tienda que publica un hecho en mercadofresco-pedido-confirmado y se desentiende. Almacén, correo y analítica tienen cada uno su cola suscrita con RawMessageDelivery=true, sus reintentos y su DLQ; cola-mercadofresco-reparto-24h recibe solo los pedidos de la franja urgente gracias a una política de filtro; el tema está cifrado con alias/mercadofresco-datos y su política restringe quién publica y con qué protocolos se puede suscribir. Sumar a márketing es ahora crear una cola y una suscripción, sin tocar ni desplegar el código de la tienda.

Por el camino han quedado claras las ideas que gobiernan el patrón: que SNS no almacena nada, lo que convierte la cola intermedia en obligatoria para todo trabajo que importe; que la cola aporta durabilidad, amortiguación y aislamiento de fallos que la suscripción directa a Lambda no puede dar; que el filtrado por atributos o por cuerpo evita el desperdicio de que todos reciban todo, con la regla de OR dentro de un atributo y AND entre atributos; que la DLQ de la suscripción y la DLQ de la cola señalan problemas distintos —entrega frente a proceso—; y que el tema alertas-mercadofresco del módulo 5 era este mismo mecanismo desde el principio, con CloudWatch como publicador que no sabe quién le escucha.

Pero el fan-out tiene un techo. SNS entrega el mismo mensaje a todos los suscriptores y solo sabe filtrar con reglas sencillas sobre lo que el publicador se acordó de incluir. Cuando MercadoFresco quiera enrutar de verdad por contenido —que PedidoCancelado vaya a un sitio y StockBajo a otro, todo sobre el mismo canal—, tendría que crear un tema por cada tipo de evento y volver a un mapa de suscripciones difícil de gobernar. Además hay hechos que la tienda no publica y que también interesan: que una instancia EC2 cambie de estado, que Trusted Advisor detecte un recurso infrautilizado, que un despliegue falle. Y falta algo que hoy duele: cuando el consumidor de analítica estuvo mal configurado, los eventos de esas horas se perdieron para siempre, porque un tema no guarda nada.

En 07-03, «Amazon EventBridge», damos el salto del tema al bus de eventos: un canal donde publican tanto las aplicaciones como los propios servicios de AWS, con reglas que enrutan según el contenido completo del evento, transformación de la entrada para no acoplar el destino al formato de origen, destinos que van desde Lambda hasta una API del ERP, tareas programadas con cron, conexión directa con los Streams de mercadofresco-carritos mediante Pipes, y —lo que habría salvado el incidente de la cola de analítica— archivo y reproducción de todo lo publicado.

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