Lab práctico · Semana 8: Desacoplamiento, serverless y contenedores
Mensajería desacoplada: SQS, fan-out con SNS, DLQ y EventBridge
Qué vas a construir
Un sistema de pedidos desacoplado, montado pieza a pieza desde CloudShell:
- Un tema SNS
lab16-pedidoscon fan-out a dos colas SQS:facturacion(recibe todo) yalmacen(solo pedidos físicos, gracias a un filtro de suscripción). - Una dead-letter queue para
facturacionconmaxReceiveCount = 2, y vas a provocar que un mensaje acabe en ella. - Long polling frente a short polling y el efecto del visibility timeout.
- Una regla de EventBridge que enruta eventos de tu aplicación a una cola
auditoriasegún su contenido.
flowchart LR
P["Productor (CLI)"] --> T["SNS lab16-pedidos"]
T -->|"todo"| QF["SQS facturacion"]
T -->|"filtro tipo = fisico"| QA["SQS almacen"]
QF -->|"tras 2 recepciones fallidas"| DLQ["SQS facturacion-dlq"]
APP["App (put-events)"] --> BUS["EventBridge default bus"]
BUS -->|"regla: source = lab16.tienda"| QAU["SQS auditoria"]
Antes de empezar
- Usuario de IAM Identity Center o IAM con MFA; no uses root.
- Región eu-south-2 (España) en CloudShell.
- Coste: SQS incluye 1 millón de peticiones al mes gratis; SNS y EventBridge cobran por millón de mensajes/eventos y aquí envías unas decenas. Colas y temas no cobran por existir, pero bórralos al acabar.
export AWS_REGION=eu-south-2
export CUENTA=$(aws sts get-caller-identity --query Account --output text)
mkdir -p ~/lab16 && cd ~/lab16
Paso 1: colas y DLQ
url_de() { aws sqs get-queue-url --queue-name "$1" --query QueueUrl --output text; }
arn_de() { aws sqs get-queue-attributes --queue-url "$1" --attribute-names QueueArn --query Attributes.QueueArn --output text; }
# DLQ con retención máxima (14 días) para tener tiempo de analizar
aws sqs create-queue --queue-name lab16-facturacion-dlq \
--attributes MessageRetentionPeriod=1209600 > /dev/null
DLQ_URL=$(url_de lab16-facturacion-dlq); DLQ_ARN=$(arn_de "$DLQ_URL")
# Cola de facturación: visibility timeout corto (10 s) para el experimento y redrive a la DLQ
jq -n --arg dlq "$DLQ_ARN" '{
VisibilityTimeout: "10",
ReceiveMessageWaitTimeSeconds: "0",
RedrivePolicy: ({deadLetterTargetArn: $dlq, maxReceiveCount: "2"} | tojson)
}' > attrs-facturacion.json
aws sqs create-queue --queue-name lab16-facturacion --attributes file://attrs-facturacion.json > /dev/null
QF_URL=$(url_de lab16-facturacion); QF_ARN=$(arn_de "$QF_URL")
# Cola de almacén con long polling activado en la propia cola (20 s)
aws sqs create-queue --queue-name lab16-almacen \
--attributes ReceiveMessageWaitTimeSeconds=20 > /dev/null
QA_URL=$(url_de lab16-almacen); QA_ARN=$(arn_de "$QA_URL")
# Cola de auditoría (destino de EventBridge)
aws sqs create-queue --queue-name lab16-auditoria > /dev/null
QAU_URL=$(url_de lab16-auditoria); QAU_ARN=$(arn_de "$QAU_URL")
aws sqs list-queues --queue-name-prefix lab16
Las colas se cifran por defecto con SSE-SQS.
Paso 2: tema SNS y permisos para escribir en las colas
TOPIC_ARN=$(aws sns create-topic --name lab16-pedidos --query TopicArn --output text)
echo "$TOPIC_ARN"
SNS escribe en las colas como principal de servicio: cada cola necesita una política de acceso (política de recurso) que lo permita solo desde este tema (aws:SourceArn).
permitir_sns() {
local url=$1 arn=$2
jq -n --arg q "$arn" --arg t "$TOPIC_ARN" '{
Version: "2012-10-17",
Statement: [{
Sid: "PermitirSNS",
Effect: "Allow",
Principal: { Service: "sns.amazonaws.com" },
Action: "sqs:SendMessage",
Resource: $q,
Condition: { ArnEquals: { "aws:SourceArn": $t } }
}]
}' > politica.json
jq -n --arg p "$(cat politica.json)" '{Policy: $p}' > attrs-politica.json
aws sqs set-queue-attributes --queue-url "$url" --attributes file://attrs-politica.json
}
permitir_sns "$QF_URL" "$QF_ARN"
permitir_sns "$QA_URL" "$QA_ARN"
Paso 3: suscripciones con fan-out y filtro
# Facturación: recibe todo, cuerpo tal cual (raw)
aws sns subscribe --topic-arn "$TOPIC_ARN" --protocol sqs \
--notification-endpoint "$QF_ARN" \
--attributes RawMessageDelivery=true
# Almacén: solo pedidos con el atributo tipo = fisico
cat > attrs-sub-almacen.json <<'EOF'
{
"RawMessageDelivery": "true",
"FilterPolicy": "{\"tipo\": [\"fisico\"]}"
}
EOF
aws sns subscribe --topic-arn "$TOPIC_ARN" --protocol sqs \
--notification-endpoint "$QA_ARN" \
--attributes file://attrs-sub-almacen.json
Publica un pedido físico y uno digital:
aws sns publish --topic-arn "$TOPIC_ARN" \
--message '{"pedido": 1001, "producto": "libro en papel"}' \
--message-attributes '{"tipo": {"DataType": "String", "StringValue": "fisico"}}'
aws sns publish --topic-arn "$TOPIC_ARN" \
--message '{"pedido": 1002, "producto": "curso online"}' \
--message-attributes '{"tipo": {"DataType": "String", "StringValue": "digital"}}'
Paso 4: consumir con long polling
# Almacén: la cola tiene long polling de 20 s; debería llegar SOLO el pedido 1001
aws sqs receive-message --queue-url "$QA_URL" --max-number-of-messages 10 \
--query 'Messages[].Body' --output text
# Facturación: long polling en la propia petición; llegan los dos pedidos
aws sqs receive-message --queue-url "$QF_URL" --max-number-of-messages 10 \
--wait-time-seconds 10 --query 'Messages[].[MessageId,Body]' --output text
Fíjate: no has borrado los mensajes de facturación. Compruébalo en las métricas aproximadas:
aws sqs get-queue-attributes --queue-url "$QF_URL" \
--attribute-names ApproximateNumberOfMessages ApproximateNumberOfMessagesNotVisible
Los dos mensajes están en vuelo (not visible) durante el visibility timeout (10 s). Si ningún consumidor los borra, vuelven a la cola.
Short polling frente a long polling: pide un mensaje a la cola de auditoría (vacía) con y sin espera y compara cuánto tarda en volver:
time aws sqs receive-message --queue-url "$QAU_URL" --wait-time-seconds 0
time aws sqs receive-message --queue-url "$QAU_URL" --wait-time-seconds 5
La primera vuelve al instante vacía (y cada vuelta vacía es una petición facturable); la segunda espera. Con consumidores que sondean en bucle, long polling reduce drásticamente las peticiones vacías.
Paso 5: provocar un mensaje venenoso y verlo en la DLQ
Simula un consumidor que falla siempre: recibe el mismo mensaje sin borrarlo. Con maxReceiveCount = 2, a la tercera recepción SQS lo mueve a la DLQ.
aws sns publish --topic-arn "$TOPIC_ARN" --message '{"pedido": "ROTO"}' \
--message-attributes '{"tipo": {"DataType": "String", "StringValue": "digital"}}'
# Espera a que caduque la visibilidad de los mensajes anteriores y "falla" varias veces
for intento in 1 2 3 4; do
sleep 12
echo "Intento $intento:"
aws sqs receive-message --queue-url "$QF_URL" --max-number-of-messages 10 \
--attribute-names ApproximateReceiveCount \
--query 'Messages[].[Body,Attributes.ApproximateReceiveCount]' --output text
done
aws sqs receive-message --queue-url "$DLQ_URL" --max-number-of-messages 10 \
--wait-time-seconds 5 --query 'Messages[].Body' --output text
Los mensajes que se recibieron dos veces sin borrarse están ahora en lab16-facturacion-dlq. En un sistema real, una alarma de CloudWatch sobre la DLQ avisaría, analizarías el error y, una vez corregido el consumidor, devolverías los mensajes a la cola original con DLQ redrive (consola de SQS → Start DLQ redrive).
Así debe trabajar un consumidor correcto: procesa y borra con el receipt handle:
aws sns publish --topic-arn "$TOPIC_ARN" --message '{"pedido": 1003}' > /dev/null
MSG=$(aws sqs receive-message --queue-url "$QF_URL" --wait-time-seconds 10 \
--query 'Messages[0].ReceiptHandle' --output text)
aws sqs delete-message --queue-url "$QF_URL" --receipt-handle "$MSG" && echo "Procesado y borrado"
Paso 6: EventBridge enruta por contenido
La cola de auditoría necesita permitir que EventBridge le escriba, limitado a la regla:
REGLA_ARN="arn:aws:events:$AWS_REGION:$CUENTA:rule/lab16-pedidos-grandes"
jq -n --arg q "$QAU_ARN" --arg r "$REGLA_ARN" '{
Version: "2012-10-17",
Statement: [{
Sid: "PermitirEventBridge",
Effect: "Allow",
Principal: { Service: "events.amazonaws.com" },
Action: "sqs:SendMessage",
Resource: $q,
Condition: { ArnEquals: { "aws:SourceArn": $r } }
}]
}' > politica-eb.json
jq -n --arg p "$(cat politica-eb.json)" '{Policy: $p}' > attrs-eb.json
aws sqs set-queue-attributes --queue-url "$QAU_URL" --attributes file://attrs-eb.json
Crea la regla en el bus por defecto: solo pedidos de la tienda con importe mayor que 100.
cat > patron.json <<'EOF'
{
"source": ["lab16.tienda"],
"detail-type": ["PedidoCreado"],
"detail": { "importe": [{ "numeric": [">", 100] }] }
}
EOF
aws events put-rule --name lab16-pedidos-grandes --event-pattern file://patron.json
aws events put-targets --rule lab16-pedidos-grandes --targets "Id=auditoria,Arn=$QAU_ARN"
Envía dos eventos, uno de 250 € y otro de 30 €:
cat > eventos.json <<'EOF'
[
{ "Source": "lab16.tienda", "DetailType": "PedidoCreado", "Detail": "{\"pedido\": 2001, \"importe\": 250}" },
{ "Source": "lab16.tienda", "DetailType": "PedidoCreado", "Detail": "{\"pedido\": 2002, \"importe\": 30}" }
]
EOF
aws events put-events --entries file://eventos.json
aws sqs receive-message --queue-url "$QAU_URL" --wait-time-seconds 10 \
--max-number-of-messages 10 --query 'Messages[].Body' --output text | jq .detail
Solo llega el pedido 2001: la regla filtra por el contenido del evento, sin código.
Comprueba que funciona
- La cola
almacenrecibió solo el pedido físico;facturacion, todos. - La DLQ contiene los mensajes que se recibieron más de dos veces sin borrarse.
- La cola
auditoriarecibió solo el evento con importe mayor que 100. - En la consola de SQS, pestaña Monitoring de cada cola, se ven los mensajes enviados y recibidos; en la de SNS, las dos suscripciones con su filtro.
Limpieza
aws events remove-targets --rule lab16-pedidos-grandes --ids auditoria
aws events delete-rule --name lab16-pedidos-grandes
aws sns delete-topic --topic-arn "$TOPIC_ARN" # borra también sus suscripciones
for url in "$QF_URL" "$QA_URL" "$QAU_URL" "$DLQ_URL"; do
aws sqs delete-queue --queue-url "$url"
done
cd ~ && rm -rf ~/lab16
(Tras borrar una cola hay que esperar 60 segundos para crear otra con el mismo nombre.)
Preguntas para pensar como arquitecto
- Los pedidos deben procesarse en orden por cliente y sin duplicados, y el almacén y la facturación deben recibir todos. ¿Qué cambiarías del diseño del lab?
Respuesta
Usar un tema SNS FIFO (.fifo) con colas SQS FIFO suscritas, publicando con MessageGroupId = identificador del cliente (orden estricto por cliente y paralelismo entre clientes) y deduplicación (ID explícito o basada en contenido). La DLQ de una cola FIFO también debe ser FIFO. Comprueba que el volumen cabe en los límites de FIFO (300 TPS por acción sin lotes, más con lotes o alto rendimiento).
- El consumidor de facturación tarda hasta 2 minutos por mensaje y el equipo ve pedidos facturados dos veces. ¿Causa y solución?
Respuesta
El visibility timeout (aquí 10 s; por defecto 30 s) es menor que el tiempo de proceso, así que el mensaje reaparece y otro consumidor lo procesa de nuevo. Solución: subir el visibility timeout por encima del tiempo máximo de proceso (o ampliarlo con ChangeMessageVisibility mientras se procesa) y hacer el consumidor idempotente, porque las colas standard entregan al menos una vez.
- Por la noche no hay pedidos y los consumidores en EC2 hacen millones de
ReceiveMessagevacíos. ¿Cómo reduces coste?
Respuesta
Long polling (ReceiveMessageWaitTimeSeconds hasta 20 s) para eliminar respuestas vacías, y escalar el Auto Scaling group según la cola acumulada por instancia (ApproximateNumberOfMessagesVisible / instancias) para reducir instancias cuando no hay trabajo. Alternativa con menos operación: sustituir los consumidores por Lambda con event source mapping, que no cobra mientras no hay mensajes que procesar.
- ¿Por qué no suscribir la función de facturación directamente al tema SNS, sin cola?
Respuesta
Porque la cola amortigua picos (la función procesa a su ritmo, con concurrencia controlada), conserva los mensajes si el consumidor falla durante horas (SNS solo reintenta durante un tiempo limitado) y ofrece DLQ, lotes y reintentos controlados. SNS → SQS → consumidor es el patrón de fan-out robusto.
- Otro equipo quiere reaccionar a los pedidos creados sin que tu servicio cambie. ¿SNS o EventBridge?
Respuesta
Ambos lo permiten sin tocar al productor (añadiendo una suscripción o una regla). EventBridge encaja mejor si el otro equipo quiere filtrar por cualquier campo del contenido, recibir también eventos de servicios de AWS o de SaaS, archivar y reproducir eventos o usar el schema registry. SNS encaja si prima el fan-out de alto volumen o la entrega a email, SMS o móviles.