Spis treści 14 sekcji
  1. Problem: jeden event, wiele reakcji
  2. Architektura: SNS → SQS → Lambda
  3. Krok 1: Utwórz SNS Topic
  4. Krok 2: Utwórz kolejki SQS
  5. Krok 3: Subskrypcje SNS → SQS
  6. Krok 4: Lambda functions
  7. Krok 5: Podłącz SQS → Lambda
  8. Krok 6: Publikuj event z aplikacji
  9. Message filtering - subskrypcje z filtrem
  10. Dead Letter Queue - gdy coś pójdzie nie tak
  11. Koszty systemu powiadomień
  12. SNS vs SQS vs EventBridge - kiedy co?
  13. FAQ - najczęstsze pytania o SNS + SQS
  14. Co dalej?
TL;DR: SNS (Simple Notification Service) to pub/sub do fan-out wiadomości, SQS (Simple Queue Service) to kolejka buforująca, Lambda to przetwarzanie. Razem tworzą event-driven architekturę, która obsługuje miliony wiadomości bez serwerów. Typowy use case: zamówienie w sklepie → SNS rozgłasza event → SQS kolejki dla email, SMS, analytics → Lambda przetwarza każdy niezależnie.
Dwa sposoby, ten sam efekt: Każdy krok poniżej możesz wykonać na dwa sposoby - klikając w panelu webowym AWS albo wpisując komendy w terminalu. Wybierz jedną metodę i trzymaj się jej. Jeśli dopiero zaczynasz, polecam Panel AWS.

Problem: jeden event, wiele reakcji

Wyobraź sobie sklep internetowy. Klient składa zamówienie. Co musi się stać?

  • Email potwierdzający do klienta
  • SMS z numerem zamówienia
  • Aktualizacja stanu magazynowego
  • Powiadomienie Slack do zespołu obsługi
  • Event do systemu analityki
  • Aktualizacja dashboardu w czasie rzeczywistym

Bez event-driven architectury każda z tych akcji to osobne wywołanie w kodzie obsługi zamówienia. Monolith rośnie, jedno powolne wysyłanie emaila blokuje cały proces, a dodanie nowej reakcji wymaga modyfikacji kodu zamówień.

Rozwiązanie: publish-subscribe pattern z SNS + SQS + Lambda.

Architektura: SNS → SQS → Lambda

Diagram architektury event-driven: SNS Topic rozgłasza event do 5 kolejek SQS (email, SMS, inventory, analytics, Slack), każda trigeruje Lambdę, nieprzetworzone wiadomości trafiają do Dead Letter Queue
SNS + SQS + Lambda - event-driven architecture pattern z fan-out, retry i DLQ.
SerwisRolaAnalogia
SNS (Topic)Rozgłoszenie eventu do wielu subskrybentówMegafon na placu
SQS (Queue)Buforowanie wiadomości, retry przy błędachSkrzynka pocztowa
Lambda (Function)Przetwarzanie każdej wiadomościPracownik przy biurku

Przepływ

  1. Aplikacja publikuje event "zamówienie złożone" do SNS Topic
  2. SNS fan-out - rozsyła event do wszystkich subskrybentów (kolejek SQS)
  3. Każda kolejka SQS buforuje event niezależnie
  4. Lambda triggerowana z SQS przetwarza wiadomość
  5. Jeśli Lambda fail → wiadomość wraca do kolejki → retry
  6. Jeśli retry wyczerpane → Dead Letter Queue (DLQ) do analizy
At-least-once delivery: SQS Standard może dostarczyć tę samą wiadomość więcej niż raz. Pisz Lambdy idempotentnie - np. zapisuj przetworzone order_id w DynamoDB i pomijaj duplikaty, żeby klient nie dostał dwóch emaili, a magazyn nie zszedł podwójnie.

Krok 1: Utwórz SNS Topic

  1. Wejdź w SNS → Topics → Create topic
  2. Type: Standard (nie FIFO - potrzebujemy fan-out)
  3. Name: order-events
  4. Kliknij Create topic
Screenshot: SNS Create Topic - order-events Standard
aws sns create-topic --name order-events
# Zanotuj TopicArn z odpowiedzi:
# arn:aws:sns:eu-central-1:123456789:order-events

Krok 2: Utwórz kolejki SQS

Tworzymy osobną kolejkę dla każdego "konsumenta" eventu + Dead Letter Queue:

  1. Wejdź w SQS → Create queue
  2. Type: Standard
  3. Name: order-email-queue
  4. Visibility timeout: 60 seconds (musi być > Lambda timeout)
  5. Message retention: 4 days
  6. Dead-letter queue: włącz, utwórz nową order-email-dlq, max receives: 3
  7. Kliknij Create queue
  8. Powtórz dla: order-sms-queue, order-inventory-queue, order-analytics-queue, order-slack-queue
# Dead Letter Queue
aws sqs create-queue --queue-name order-email-dlq

# Główna kolejka z DLQ
aws sqs create-queue --queue-name order-email-queue \
  --attributes '{
    "VisibilityTimeout": "60",
    "MessageRetentionPeriod": "345600",
    "RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:eu-central-1:123456789:order-email-dlq\",\"maxReceiveCount\":\"3\"}"
  }'

# Kolejka SMS
aws sqs create-queue --queue-name order-sms-queue \
  --attributes '{
    "VisibilityTimeout": "60",
    "RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:eu-central-1:123456789:order-sms-dlq\",\"maxReceiveCount\":\"3\"}"
  }'

# Kolejka inventory
aws sqs create-queue --queue-name order-inventory-queue \
  --attributes '{
    "VisibilityTimeout": "60",
    "RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:eu-central-1:123456789:order-inventory-dlq\",\"maxReceiveCount\":\"3\"}"
  }'

# Kolejka analytics
aws sqs create-queue --queue-name order-analytics-queue

# Kolejka Slack
aws sqs create-queue --queue-name order-slack-queue

Krok 3: Subskrypcje SNS → SQS

  1. Wejdź w SNS → Topics → order-events → Create subscription
  2. Protocol: Amazon SQS
  3. Endpoint: ARN kolejki order-email-queue
  4. Kliknij Create subscription
  5. Powtórz dla pozostałych kolejek
  6. Ważne: Na stronie SQS, każda kolejka musi mieć Access Policy pozwalającą SNS na wysyłanie wiadomości
# Subskrypcja email queue
aws sns subscribe \
  --topic-arn arn:aws:sns:eu-central-1:123456789:order-events \
  --protocol sqs \
  --notification-endpoint arn:aws:sqs:eu-central-1:123456789:order-email-queue

# Subskrypcja SMS queue
aws sns subscribe \
  --topic-arn arn:aws:sns:eu-central-1:123456789:order-events \
  --protocol sqs \
  --notification-endpoint arn:aws:sqs:eu-central-1:123456789:order-sms-queue

# Subskrypcja inventory queue
aws sns subscribe \
  --topic-arn arn:aws:sns:eu-central-1:123456789:order-events \
  --protocol sqs \
  --notification-endpoint arn:aws:sqs:eu-central-1:123456789:order-inventory-queue

# Subskrypcja analytics queue
aws sns subscribe \
  --topic-arn arn:aws:sns:eu-central-1:123456789:order-events \
  --protocol sqs \
  --notification-endpoint arn:aws:sqs:eu-central-1:123456789:order-analytics-queue

# Subskrypcja Slack queue
aws sns subscribe \
  --topic-arn arn:aws:sns:eu-central-1:123456789:order-events \
  --protocol sqs \
  --notification-endpoint arn:aws:sqs:eu-central-1:123456789:order-slack-queue

# Access policy na kolejce SQS (pozwól SNS wysyłać)
# Powtórz dla KAŻDEJ kolejki - bez tej policy SNS po cichu nie dostarczy do niej wiadomości
aws sqs set-queue-attributes \
  --queue-url https://sqs.eu-central-1.amazonaws.com/123456789/order-email-queue \
  --attributes '{
    "Policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"sns.amazonaws.com\"},\"Action\":\"sqs:SendMessage\",\"Resource\":\"arn:aws:sqs:eu-central-1:123456789:order-email-queue\",\"Condition\":{\"ArnEquals\":{\"aws:SourceArn\":\"arn:aws:sns:eu-central-1:123456789:order-events\"}}}]}"
  }'

Krok 4: Lambda functions

Lambda: wysyłka emaila

import json
import boto3

ses = boto3.client('ses', region_name='eu-central-1')

def handler(event, context):
    for record in event['Records']:
        # SQS body zawiera SNS message
        sns_message = json.loads(record['body'])
        order = json.loads(sns_message['Message'])

        order_id = order['order_id']
        email = order['customer_email']
        total = order['total']
        items = order['items']

        items_html = "".join(
            f"<li>{item['name']} x{item['qty']} - {item['price']} zł</li>"
            for item in items
        )

        ses.send_email(
            Source='[email protected]',
            Destination={'ToAddresses': [email]},
            Message={
                'Subject': {'Data': f'Potwierdzenie zamówienia #{order_id}'},
                'Body': {
                    'Html': {'Data': f"""
                        <h2>Dziękujemy za zamówienie!</h2>
                        <p>Numer: <strong>#{order_id}</strong></p>
                        <ul>{items_html}</ul>
                        <p>Suma: <strong>{total} zł</strong></p>
                    """}
                }
            }
        )
        print(f"Email wysłany do {email} dla zamówienia #{order_id}")
SES sandbox: nowe konto SES działa w trybie sandbox - wyślesz emaile tylko na zweryfikowane adresy. Zanim system zadziała dla prawdziwych klientów, poproś o production access w konsoli SES (Account dashboard → Request production access, decyzja zwykle w 24h).

Lambda: aktualizacja magazynu

import json
import boto3

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Inventory')

def handler(event, context):
    for record in event['Records']:
        sns_message = json.loads(record['body'])
        order = json.loads(sns_message['Message'])

        for item in order['items']:
            # Atomowe zmniejszenie stanu magazynowego
            table.update_item(
                Key={'product_id': item['product_id']},
                UpdateExpression='SET stock = stock - :qty',
                ConditionExpression='stock >= :qty',
                ExpressionAttributeValues={
                    ':qty': item['qty']
                }
            )
            print(f"Magazyn zaktualizowany: {item['product_id']} (-{item['qty']})")

Lambda: Slack notification

import json
import os
import urllib3

# Webhook to sekret - trzymaj go w zmiennych środowiskowych Lambdy, nie w kodzie
SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL']

def handler(event, context):
    http = urllib3.PoolManager()

    for record in event['Records']:
        sns_message = json.loads(record['body'])
        order = json.loads(sns_message['Message'])

        msg = {
            "text": f":shopping_cart: Nowe zamówienie #{order['order_id']}!",
            "blocks": [
                {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": f"*Zamówienie #{order['order_id']}*\n"
                                f"Klient: {order['customer_email']}\n"
                                f"Suma: *{order['total']} zł*\n"
                                f"Produkty: {len(order['items'])}"
                    }
                }
            ]
        }

        http.request('POST', SLACK_WEBHOOK_URL,
            body=json.dumps(msg).encode('utf-8'),
            headers={'Content-Type': 'application/json'}
        )
        print(f"Slack notification wysłany dla #{order['order_id']}")

Krok 5: Podłącz SQS → Lambda

# Event source mapping: SQS → Lambda
aws lambda create-event-source-mapping \
  --function-name order-send-email \
  --event-source-arn arn:aws:sqs:eu-central-1:123456789:order-email-queue \
  --batch-size 10 \
  --maximum-batching-window-in-seconds 5

aws lambda create-event-source-mapping \
  --function-name order-update-inventory \
  --event-source-arn arn:aws:sqs:eu-central-1:123456789:order-inventory-queue \
  --batch-size 1

aws lambda create-event-source-mapping \
  --function-name order-slack-notify \
  --event-source-arn arn:aws:sqs:eu-central-1:123456789:order-slack-queue \
  --batch-size 10

Krok 6: Publikuj event z aplikacji

import boto3
import json
from datetime import datetime

sns = boto3.client('sns', region_name='eu-central-1')
TOPIC_ARN = 'arn:aws:sns:eu-central-1:123456789:order-events'

def create_order(order_data):
    """Wywoływane z API/Lambda obsługującej zamówienia"""

    # Zapisz zamówienie w bazie (DynamoDB/RDS)
    order_id = save_to_database(order_data)

    # Publikuj event - SNS rozsyła do wszystkich subskrybentów
    sns.publish(
        TopicArn=TOPIC_ARN,
        Message=json.dumps({
            'order_id': order_id,
            'customer_email': order_data['email'],
            'customer_phone': order_data.get('phone'),
            'items': order_data['items'],
            'total': order_data['total'],
            'timestamp': datetime.utcnow().isoformat()
        }),
        MessageAttributes={
            'event_type': {
                'DataType': 'String',
                'StringValue': 'order.created'
            },
            'order_value': {
                'DataType': 'Number',
                'StringValue': str(order_data['total'])
            }
        }
    )

    return order_id

Message filtering - subskrypcje z filtrem

Nie każda kolejka musi dostawać każdy event. SNS pozwala filtrować wiadomości na podstawie atrybutów:

# Kolejka SMS dostaje eventy tylko dla zamówień > 500 zł
aws sns set-subscription-attributes \
  --subscription-arn arn:aws:sns:eu-central-1:123456789:order-events:sub-sms \
  --attribute-name FilterPolicy \
  --attribute-value '{
    "order_value": [{"numeric": [">=", 500]}]
  }'

# Kolejka VIP dostaje eventy tylko typu "order.created" i "order.shipped"
aws sns set-subscription-attributes \
  --subscription-arn arn:aws:sns:eu-central-1:123456789:order-events:sub-vip \
  --attribute-name FilterPolicy \
  --attribute-value '{
    "event_type": ["order.created", "order.shipped"]
  }'

Dead Letter Queue - gdy coś pójdzie nie tak

DLQ zbiera wiadomości, których Lambda nie mogła przetworzyć po 3 próbach (maxReceiveCount). To Twoja siatka bezpieczeństwa:

# Sprawdź ile wiadomości jest w DLQ
aws sqs get-queue-attributes \
  --queue-url https://sqs.eu-central-1.amazonaws.com/123456789/order-email-dlq \
  --attribute-names ApproximateNumberOfMessages

# Skrypt do przeglądania DLQ
import boto3, json

sqs = boto3.client('sqs', region_name='eu-central-1')
DLQ_URL = 'https://sqs.eu-central-1.amazonaws.com/123456789/order-email-dlq'

response = sqs.receive_message(
    QueueUrl=DLQ_URL,
    MaxNumberOfMessages=10,
    MessageAttributeNames=['All']
)

for msg in response.get('Messages', []):
    body = json.loads(msg['Body'])
    print(f"Failed message: {json.dumps(body, indent=2)}")
    # Po naprawieniu: sqs.delete_message(QueueUrl=DLQ_URL, ReceiptHandle=msg['ReceiptHandle'])
Pro tip: Ustaw CloudWatch Alarm na metrykę ApproximateNumberOfMessagesVisible w DLQ. Jeśli > 0, dostajesz powiadomienie. Zerowa tolerancja na wiadomości w DLQ.

Koszty systemu powiadomień

SerwisFree TierKoszt (100K eventów/mc)
SNS (publish)1M requestów/mc$0 (mieści się w free)
SNS → SQS (delivery)Unlimited$0 (SNS→SQS jest free)
SQS (standard)1M requestów/mc~$0.04
Lambda (4 funkcje × 100K)1M invocations/mc$0 (mieści się w free)
SES (emaile)3K emaili/mc przez pierwsze 12 mies.~$10 za 100K ($0.10/1K)
SUMA~$10-11/mc

System obsługujący 100,000 zamówień miesięcznie za ~$10-11 (prawie wszystko to SES). Spróbuj to zrobić taniej z własnym serwerem RabbitMQ.

SNS vs SQS vs EventBridge - kiedy co?

SerwisPatternKiedy używać
SNSPub/Sub (fan-out)Jeden event → wiele reakcji jednocześnie
SQSQueue (point-to-point)Buforowanie, retry, rate limiting, decoupling
EventBridgeEvent bus (routing)Złożone reguły routingu, integracja z SaaS, scheduling
SNS + SQSFan-out + bufferNiezawodny fan-out z retry per konsument (nasz use case)

FAQ - najczęstsze pytania o SNS + SQS

Dlaczego SNS + SQS a nie samo SNS → Lambda?

SNS może triggerować Lambda bezpośrednio, ale bez SQS nie masz: retry z backoff, batch processing, DLQ per konsument, rate limiting. Jeśli Lambda fail przy SNS→Lambda, masz tylko 2 automatyczne retry (asynchroniczne wywołanie Lambdy), a potem wiadomość przepada - chyba że skonfigurujesz DLQ na subskrypcji. Przy SNS→SQS→Lambda wiadomość wraca do kolejki i jest ponawiana w kontrolowany sposób. Dla produkcji zawsze dodawaj SQS między.

Standard vs FIFO - kiedy wybrać FIFO?

FIFO gwarantuje kolejność i exactly-once delivery. Użyj FIFO gdy kolejność jest krytyczna (np. transakcje bankowe: debit musi być przed credit). Dla powiadomień Standard wystarczy - email może przyjść o sekundę szybciej lub wolniej i nikt nie zauważy. FIFO ma niższy throughput (3000 msg/s vs unlimited).

Co się stanie jeśli Lambda jest wyłączona a wiadomości przychodzą?

Wiadomości czekają w kolejce SQS. Domyślna retencja to 4 dni (max 14 dni). Gdy Lambda wstanie - przetworzy zalegające wiadomości. To główna zaleta kolejki - buforuje gdy konsument jest niedostępny.

Jak dodać nowego konsumenta (np. nowy typ powiadomienia)?

Utwórz nową kolejkę SQS, subskrybuj do istniejącego SNS Topic, stwórz nową Lambda. Zero zmian w kodzie aplikacji publikującej eventy. To sedno event-driven architecture - producent nie wie kto konsumuje.

Co dalej?

  1. Utwórz SNS Topic i 2-3 kolejki SQS z DLQ.
  2. Napisz prostą Lambda przetwarzającą wiadomości i podepnij do SQS.
  3. Opublikuj testowy event i sprawdź czy przechodzi przez cały pipeline.
  4. Dodaj CloudWatch alarm na DLQ - zero tolerancji na utracone wiadomości.
  5. Rozbudowuj o Message Filtering i nowych konsumentów.

Event-driven architecture to fundament skalowalnych aplikacji na AWS. SNS + SQS + Lambda to trio, które obsługuje od kilku do milionów eventów dziennie bez zmian w architekturze. Jeśli chcesz zobaczyć te serwisy w kontekście pełnej aplikacji, sprawdź Use Case: SaaS za 200 zł lub architekturę webową na AWS.

Emil Kowalczyk

Pasjonat chmury i twórca CloudManiak.pl. Na co dzień MSP Engineer w amerykańskiej firmie ClearScale (AWS Premier Tier Partner). Pomagam osobom wchodzącym do świata chmury zdobywać wiedzę i certyfikaty.