Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Run on LocalStack

on:
push:
paths-ignore:
- '**.md'
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:

permissions:
contents: read

jobs:
example:
name: ${{ matrix.example }}
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
example:
- ses
- sqs
- sns
defaults:
run:
working-directory: ${{ matrix.example }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22

- name: Install lstk
run: |
npm install -g @localstack/lstk
lstk setup aws --force --non-interactive

- name: Install dependencies
if: matrix.example != 'ses'
run: npm ci

- name: Start LocalStack
env:
LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }}
run: lstk start

- name: Deploy the CDK stack
if: matrix.example != 'ses'
run: make deploy

- name: Run tests
run: make test

- name: Show LocalStack logs
if: failure()
run: lstk logs | tail -300
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# AWS Deep Dives

This repository will contain a series of examples that will dig into using popular AWS services. Each example will be paired with a blog post on the LocalStack blog. Currently, the only examples are emulating SES and SQS on LocalStack.
This repository will contain a series of examples that will dig into using popular AWS services. Each example will be paired with a blog post on the LocalStack blog. Currently, the examples emulate SES, SQS and SNS on LocalStack:

- [`ses`](ses): send email through a verified identity with a configuration set and SNS event notifications
- [`sqs`](sqs): a FIFO queue with retries and a dead-letter queue
- [`sns`](sns): fan-out to Lambda subscribers with message filtering

Each example has a `make test` target that checks it works, and CI runs all three on LocalStack.

## Prerequisites

Expand Down
10 changes: 8 additions & 2 deletions ses/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@ start:
@test -n "$(LOCALSTACK_AUTH_TOKEN)" || (echo "LOCALSTACK_AUTH_TOKEN is not set. Please set it before starting LocalStack." && exit 1)
LOCALSTACK_DEBUG=1 LOCALSTACK_AUTH_TOKEN=$(LOCALSTACK_AUTH_TOKEN) lstk start

.PHONY: test
test:
./test.sh

.PHONY: help
help:
@echo "Available commands:"
@echo " start - Start LocalStack (requires LOCALSTACK_AUTH_TOKEN)"
@echo " test - Run the setup, send emails and check the results"
@echo " create-config-set - Create the SES configuration set"
@echo " create-sns-topic - Create SNS topic for SES events"
@echo " subscribe-topic - Subscribe to the SNS topic via email"
Expand Down Expand Up @@ -52,7 +57,7 @@ create-config-set:
# for example purposes, we're only including limited events
.PHONY: setup-event-dest
setup-event-dest:
$(eval SNS_TOPIC_ARN := $(shell lstk aws sns get-topic-attributes --topic-arn arn:aws:sns:$(shell lstk aws configure get region):$(shell lstk aws sts get-caller-identity --query Account --output text):$(SNS_TOPIC_NAME) --query 'Attributes.TopicArn' --output text 2>/dev/null || aws sns create-topic --name $(SNS_TOPIC_NAME) --query 'TopicArn' --output text))
$(eval SNS_TOPIC_ARN := $(shell lstk aws sns create-topic --name $(SNS_TOPIC_NAME) --query 'TopicArn' --output text))
@echo "Setting up event destination to SNS topic: $(SNS_TOPIC_ARN)"
lstk aws ses create-configuration-set-event-destination \
--configuration-set-name $(CONFIG_SET_NAME) \
Expand All @@ -67,8 +72,9 @@ verify-email:

# Subscribe to the SNS topic
# As we are testing this locally on LocalStack, this will also automatically confirm the subscription
.PHONY: subscribe-topic
subscribe-topic:
$(eval SNS_TOPIC_ARN := $(shell lstk aws sns get-topic-attributes --topic-arn arn:aws:sns:$(shell lstk aws configure get region):$(shell lstk aws sts get-caller-identity --query Account --output text):$(SNS_TOPIC_NAME) --query 'Attributes.TopicArn' --output text 2>/dev/null || aws sns create-topic --name $(SNS_TOPIC_NAME) --query 'TopicArn' --output text))
$(eval SNS_TOPIC_ARN := $(shell lstk aws sns create-topic --name $(SNS_TOPIC_NAME) --query 'TopicArn' --output text))
@echo "Subscribing to SNS topic: $(SNS_TOPIC_ARN)"
SUBSCRIPTION_ARN=$$(lstk aws sns subscribe --topic-arn $(SNS_TOPIC_ARN) --protocol email --notification-endpoint $(SNS_TOPIC_EMAIL) | jq -r '.SubscriptionArn') && \
SUBSCRIPTION_TOKEN=$$(curl "http://localhost:4566/_aws/sns/subscription-tokens/$$SUBSCRIPTION_ARN" | jq -r '.subscription_token') && \
Expand Down
10 changes: 10 additions & 0 deletions ses/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ To set up SES with a verified email identity, a configuration set and an SNS top
make setup-all
```

## Test

To run the full setup, send a text and an HTML email, and check the results:

```bash
make test
```

The test checks the sender is verified, the configuration set exists, the email subscription is confirmed, and both emails reach LocalStack's SES message store (`/_aws/ses`). Run it against a fresh LocalStack, since `make setup-all` fails if the configuration set already exists.

## Available Commands

Note that, while you can run each command individually, some commands are dependent on other services existing, so the order you run them is important.
Expand Down
58 changes: 58 additions & 0 deletions ses/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/bin/bash
# Run the README's setup and send flow, then check:
# - the sender identity is verified
# - the configuration set exists
# - the email subscription on the topic is confirmed
# - the text and HTML emails were sent, read back from LocalStack's SES message store
set -euo pipefail

ENDPOINT=${ENDPOINT:-http://localhost:4566}
SENDER=$(sed -n 's/^EMAIL_IDENTITY = //p' Makefile)
CONFIG_SET=sample-ses-config-set
TOPIC_NAME=ses-events-topic
TO_EMAIL="ci-$(date +%s)@example.com"

make setup-all

status=$(lstk aws ses get-identity-verification-attributes --identities "$SENDER" \
--query "VerificationAttributes.\"$SENDER\".VerificationStatus" --output text)
if [ "$status" != "Success" ]; then
echo "Test failed: $SENDER verification status is $status"
exit 1
fi
echo "PASS: $SENDER is verified"

# make setup-all fails if creating the SNS event destination fails. Reading it back is
# not possible on LocalStack yet: describe-configuration-set returns no EventDestinations
# and sesv2 get-configuration-set-event-destinations is not implemented.
if ! lstk aws ses describe-configuration-set --configuration-set-name "$CONFIG_SET" > /dev/null; then
echo "Test failed: configuration set $CONFIG_SET not found"
exit 1
fi
echo "PASS: configuration set $CONFIG_SET exists"

TOPIC_ARN=$(lstk aws sns create-topic --name "$TOPIC_NAME" --query TopicArn --output text)
subscription=$(lstk aws sns list-subscriptions-by-topic --topic-arn "$TOPIC_ARN" \
--query "Subscriptions[?Protocol=='email'] | [0].SubscriptionArn" --output text)
if [ -z "$subscription" ] || [ "$subscription" = "None" ] || [ "$subscription" = "PendingConfirmation" ]; then
echo "Test failed: the email subscription is not confirmed ($subscription)"
exit 1
fi
echo "PASS: email subscription confirmed"

make send-email TO_EMAIL="$TO_EMAIL"
make send-html-email TO_EMAIL="$TO_EMAIL"

messages=$(curl -s "$ENDPOINT/_aws/ses?email=$SENDER" | jq --arg to "$TO_EMAIL" \
'[.messages[] | select(.Destination.ToAddresses | index($to))]')
if ! echo "$messages" | jq -e 'length == 2 and all(.Subject == "Test Email from SES")' > /dev/null; then
echo "Test failed: expected 2 emails to $TO_EMAIL, found $messages"
exit 1
fi
if ! echo "$messages" | jq -e 'any(.Body.html_part // "" | contains("<strong>HTML email</strong>"))' > /dev/null; then
echo "Test failed: the HTML email body was not stored: $messages"
exit 1
fi
echo "PASS: text and HTML emails sent to $TO_EMAIL"

echo "Test passed: SES is configured and sends email"
8 changes: 8 additions & 0 deletions sns/Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
LOCALSTACK_AUTH_TOKEN ?= $(LOCALSTACK_AUTH_TOKEN)

# lstk cdk runs the cdk CLI from PATH, so use the version pinned in package.json
export PATH := $(CURDIR)/node_modules/.bin:$(PATH)

.PHONY: start
start:
@test -n "$(LOCALSTACK_AUTH_TOKEN)" || (echo "LOCALSTACK_AUTH_TOKEN is not set. Please set it before starting LocalStack." && exit 1)
Expand All @@ -10,8 +13,13 @@ deploy:
lstk cdk bootstrap
lstk cdk deploy --require-approval never

.PHONY: test
test:
./test.sh

.PHONY: help
help:
@echo "Available commands:"
@echo " start - Start LocalStack (requires LOCALSTACK_AUTH_TOKEN)"
@echo " deploy - Deploy the SNS stack to LocalStack using CDK"
@echo " test - Publish order events and check the filter policies"
24 changes: 16 additions & 8 deletions sns/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ npm install
make deploy
```

Then check the subscribers:

```bash
make test
```

The test publishes OrderPlaced, low and high priority OrderDelayed, and high priority OrderCancelled events through the API. It checks that each Lambda's logs only contain the events its filter policy allows, that ops-alerts published to the `ops-alerts` topic, and that customer-notifications sent SMS messages (read from LocalStack's `/_aws/sns/sms-messages` endpoint).

### Manual Deployment

Deploy the CDK stack:
Expand Down Expand Up @@ -91,7 +99,7 @@ curl -X POST https://<api-id>.execute-api.localhost.localstack.cloud:4566/prod/o
-d '{
"eventType": "OrderPlaced",
"userId": "user-123",
"phoneNumber": "3333333333",
"phoneNumber": "+13333333333",
"orderTotal": 99.99,
"items": [
{"productId": "PROD-001", "quantity": 2, "price": 49.99}
Expand All @@ -106,7 +114,7 @@ curl -X POST https://<api-id>.execute-api.localhost.localstack.cloud:4566/prod/o
-d '{
"eventType": "OrderShipped",
"userId": "user-123",
"phoneNumber": "3333333333",
"phoneNumber": "+13333333333",
"trackingNumber": "1Z999AA10123456784",
"estimatedDelivery": "2024-01-15"
}'
Expand All @@ -119,20 +127,20 @@ curl -X POST https://<api-id>.execute-api.localhost.localstack.cloud:4566/prod/o
-d '{
"eventType": "OrderDelayed",
"userId": "user-123",
"phoneNumber": "3333333333",
"phoneNumber": "+13333333333",
"priority": "high",
"reason": "Weather delay at distribution center"
}'
```

**Cancel an order (high priority - triggers ops alert):**
```bash
curl -X POST https://6epzwkqhxp.execute-api.localhost.localstack.cloud:4566/prod/orders/ORD-12345/events \
curl -X POST https://<api-id>.execute-api.localhost.localstack.cloud:4566/prod/orders/ORD-12345/events \
-H "Content-Type: application/json" \
-d '{
"eventType": "OrderCancelled",
"userId": "user-123",
"phoneNumber": "3333333333",
"phoneNumber": "+13333333333",
"priority": "high",
"reason": "Item out of stock"
}'
Expand Down Expand Up @@ -176,18 +184,18 @@ Each message includes attributes for filtering:

1. **Low priority delay** - Only Analytics receives it:
```bash
curl -X POST <api-url>/orders \
curl -X POST <api-url>/orders -H "Content-Type: application/json" \
-d '{"eventType": "OrderDelayed", "userId": "test", "priority": "low"}'
```

2. **High priority delay** - Analytics AND Ops Alerts receive it:
```bash
curl -X POST <api-url>/orders \
curl -X POST <api-url>/orders -H "Content-Type: application/json" \
-d '{"eventType": "OrderDelayed", "userId": "test", "priority": "high"}'
```

3. **Order placed** - Customer Notifications AND Analytics receive it:
```bash
curl -X POST <api-url>/orders \
curl -X POST <api-url>/orders -H "Content-Type: application/json" \
-d '{"eventType": "OrderPlaced", "userId": "test", "orderTotal": 50}'
```
Loading
Loading