tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.List working directory contents
ls -la
total 8 drwxr-xr-x 2 root root 4096 Apr 21 20:05 . drwxr-xr-x 1 root root 4096 Apr 28 14:22 .. [stdout] total 8 drwxr-xr-x 2 root root 4096 Apr 21 20:05 . drwxr-xr-x 1 root root 4096 Apr 28 14:22 ..
Check tool versions
aws --version && python3 --version && which jq zip
aws-cli/2.34.33 Python/3.14.4 Linux/6.10.14-linuxkit exe/aarch64.ubuntu.24 Python 3.12.3 /usr/bin/jq /usr/bin/zip [stdout] aws-cli/2.34.33 Python/3.14.4 Linux/6.10.14-linuxkit exe/aarch64.ubuntu.24 Python 3.12.3 /usr/bin/jq /usr/bin/zip
Check localstack health
curl -s http://localstack:4566/_localstack/health | jq .
{
"services": {
"acm": "disabled",
"apigateway": "disabled",
"cloudformation": "disabled",
"cloudwatch": "disabled",
"config": "disabled",
"dynamodb": "available",
"dynamodbstreams": "available",
"ec2": "disabled",
"es": "disabled",
"events": "available",
"firehose": "disabled",
"iam": "available",
"kinesis": "available",
"kms": "available",
"lambda": "available",
"logs": "available",
"opensearch": "disabled",
"redshift": "disabled",
"resource-groups": "disabled",
"resourcegroupstaggingapi": "disabled",
"route53": "disabled",
"route53resolver": "disabled",
"s3": "available",
"s3control": "disabled",
"scheduler": "disabled",
"secretsmanager": "disabled",
"ses": "disabled",
"sns": "disabled",
"sqs": "available",
"ssm": "disabled",
"stepfunctions": "disabled",
"sts": "available",
"support": "disabled",
"swf": "disabled",
"transcribe": "disabled"
},
"edition": "community",
"version": "4.13.1"
}
[stdout]
{
"services": {
"acm": "disabled",
"apigateway": "disabled",
"cloudformation": "disabled",
"cloudwatch": "disabled",
"config": "disabled",
"dynamodb": "available",
"dynamodbstreams": "available",
"ec2": "disabled",
"es": "disabled",
"events": "available",
"firehose": "disabled",
"iam": "available",
"kinesis": "available",
"kms": "available",
"lambda": "available",
"logs": "available",
"opensearch": "disabled",
"redshift": "disabled",
"resource-groups": "disabled",
"resourcegroupstaggingapi": "disabled",
"route53": "disabled",
"route53resolver": "disabled",
"s3": "available",
"s3control": "disabled",
"scheduler": "disabled",
"secretsmanager": "disabled",
"ses": "disabled",
"sns": "disabled",
"sqs": "available",
"ssm": "disabled",
"stepfunctions": "disabled",
"sts": "available",
"support": "disabled",
"swf": "disabled",
"transcribe": "disabled"
},
"edition": "community",
"version": "4.13.1"
}Verify AWS creds and localstack access
echo $AWS_ACCESS_KEY_ID $AWS_DEFAULT_REGION; aws --endpoint-url=http://localstack:4566 sts get-caller-identity
test us-east-1
{
"UserId": "AKIAIOSFODNN7EXAMPLE",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
[stdout]
test us-east-1
{
"UserId": "AKIAIOSFODNN7EXAMPLE",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}Create build directories
mkdir -p /app/build /app/lambdas/order-api /app/lambdas/outbox-relay /app/policies
(Bash completed with no output)
Create KMS CMK and alias
export AWS_PAGER="" export ENDPOINT=http://localstack:4566 # Create KMS CMK KEY_ID=$(aws --endpoint-url=$ENDPOINT kms create-key --description "outbox cmk" --query 'KeyMetadata.KeyId' --output text) echo "KEY_ID=$KEY_ID" # Create alias aws --endpoint-url=$ENDPOINT kms create-alias --alias-name alias/outbox-cmk --target-key-id $KEY_ID echo "alias created" # Get full ARN KEY_ARN=$(aws --endpoint-url=$ENDPOINT kms describe-key --key-id alias/outbox-cmk --query 'KeyMetadata.Arn' --output text) echo "KEY_ARN=$KEY_ARN" echo $KEY_ARN > /app/build/key_arn.txt echo $KEY_ID > /app/build/key_id.txt
KEY_ID=a5ae8978-f0fd-4af3-a3fc-80c5235b27fa alias created KEY_ARN=arn:aws:kms:us-east-1:000000000000:key/a5ae8978-f0fd-4af3-a3fc-80c5235b27fa [stdout] KEY_ID=a5ae8978-f0fd-4af3-a3fc-80c5235b27fa alias created KEY_ARN=arn:aws:kms:us-east-1:000000000000:key/a5ae8978-f0fd-4af3-a3fc-80c5235b27fa
Create DynamoDB tables with CMK encryption and stream
export AWS_PAGER="" export ENDPOINT=http://localstack:4566 KEY_ARN=$(cat /app/build/key_arn.txt) # Create orders table - encrypted with CMK aws --endpoint-url=$ENDPOINT dynamodb create-table \ --table-name orders \ --attribute-definitions AttributeName=order_id,AttributeType=S \ --key-schema AttributeName=order_id,KeyType=HASH \ --billing-mode PAY_PER_REQUEST \ --sse-specification "Enabled=true,SSEType=KMS,KMSMasterKeyId=$KEY_ARN" \ --query 'TableDescription.TableArn' --output text # Create outbox table - with stream NEW_AND_OLD_IMAGES, encrypted with CMK aws --endpoint-url=$ENDPOINT dynamodb create-table \ --table-name outbox \ --attribute-definitions AttributeName=event_id,AttributeType=S \ --key-schema AttributeName=event_id,KeyType=HASH \ --billing-mode PAY_PER_REQUEST \ --stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES \ --sse-specification "Enabled=true,SSEType=KMS,KMSMasterKeyId=$KEY_ARN" \ --query 'TableDescription.TableArn' --output text # Create processed_events dedup table - with TTL, encrypted aws --endpoint-url=$ENDPOINT dynamodb create-table \ --table-name processed_events \ --attribute-definitions AttributeName=event_id,AttributeType=S \ --key-schema AttributeName=event_id,KeyType=HASH \ --billing-mode PAY_PER_REQUEST \ --sse-specification "Enabled=true,SSEType=KMS,KMSMasterKeyId=$KEY_ARN" \ --query 'TableDescription.TableArn' --output text # Wait for tables to become active for t in orders outbox processed_events; do aws --endpoint-url=$ENDPOINT dynamodb wait table-exists --table-name $t done # Enable TTL on processed_events aws --endpoint-url=$ENDPOINT dynamodb update-time-to-live \ --table-name processed_events \ --time-to-live-specification "Enabled=true, AttributeName=ttl" echo "tables ready" aws --endpoint-url=$ENDPOINT dynamodb describe-table --table-name outbox --query 'Table.LatestStreamArn' --output text > /app/build/outbox_stream_arn.txt echo "stream: $(cat /app/build/outbox_stream_arn.txt)"
arn:aws:dynamodb:us-east-1:000000000000:table/orders
arn:aws:dynamodb:us-east-1:000000000000:table/outbox
arn:aws:dynamodb:us-east-1:000000000000:table/processed_events
{
"TimeToLiveSpecification": {
"Enabled": true,
"AttributeName": "ttl"
}
}
tables ready
stream: arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T14:25:37.799
[stdout]
arn:aws:dynamodb:us-east-1:000000000000:table/orders
arn:aws:dynamodb:us-east-1:000000000000:table/outbox
arn:aws:dynamodb:us-east-1:000000000000:table/processed_events
{
"TimeToLiveSpecification": {
"Enabled": true,
"AttributeName": "ttl"
}
}
tables ready
stream: arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T14:25:37.799Create EventBridge bus and SQS queues with DLQs
export AWS_PAGER=""
export ENDPOINT=http://localstack:4566
KEY_ARN=$(cat /app/build/key_arn.txt)
# Create EventBridge custom bus
aws --endpoint-url=$ENDPOINT events create-event-bus \
--name order-events \
--kms-key-identifier $KEY_ARN \
--query 'EventBusArn' --output text > /app/build/bus_arn.txt
echo "bus: $(cat /app/build/bus_arn.txt)"
# Create DLQs first (with CMK)
for q in orders-created-dlq orders-cancelled-dlq; do
aws --endpoint-url=$ENDPOINT sqs create-queue \
--queue-name $q \
--attributes "{\"KmsMasterKeyId\":\"$KEY_ARN\"}" \
--query 'QueueUrl' --output text
done
CREATED_DLQ_URL=$(aws --endpoint-url=$ENDPOINT sqs get-queue-url --queue-name orders-created-dlq --query 'QueueUrl' --output text)
CANCELLED_DLQ_URL=$(aws --endpoint-url=$ENDPOINT sqs get-queue-url --queue-name orders-cancelled-dlq --query 'QueueUrl' --output text)
CREATED_DLQ_ARN=$(aws --endpoint-url=$ENDPOINT sqs get-queue-attributes --queue-url $CREATED_DLQ_URL --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
CANCELLED_DLQ_ARN=$(aws --endpoint-url=$ENDPOINT sqs get-queue-attributes --queue-url $CANCELLED_DLQ_URL --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
echo $CREATED_DLQ_ARN > /app/build/created_dlq_arn.txt
echo $CANCELLED_DLQ_ARN > /app/build/cancelled_dlq_arn.txt
# Create main queues, each with its own redrive policy pointing to its own DLQ
aws --endpoint-url=$ENDPOINT sqs create-queue \
--queue-name orders-created \
--attributes "{\"KmsMasterKeyId\":\"$KEY_ARN\",\"RedrivePolicy\":\"{\\\"deadLetterTargetArn\\\":\\\"$CREATED_DLQ_ARN\\\",\\\"maxReceiveCount\\\":\\\"5\\\"}\"}" \
--query 'QueueUrl' --output text
aws --endpoint-url=$ENDPOINT sqs create-queue \
--queue-name orders-cancelled \
--attributes "{\"KmsMasterKeyId\":\"$KEY_ARN\",\"RedrivePolicy\":\"{\\\"deadLetterTargetArn\\\":\\\"$CANCELLED_DLQ_ARN\\\",\\\"maxReceiveCount\\\":\\\"5\\\"}\"}" \
--query 'QueueUrl' --output text
CREATED_URL=$(aws --endpoint-url=$ENDPOINT sqs get-queue-url --queue-name orders-created --query 'QueueUrl' --output text)
CANCELLED_URL=$(aws --endpoint-url=$ENDPOINT sqs get-queue-url --queue-name orders-cancelled --query 'QueueUrl' --output text)
CREATED_ARN=$(aws --endpoint-url=$ENDPOINT sqs get-queue-attributes --queue-url $CREATED_URL --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
CANCELLED_ARN=$(aws --endpoint-url=$ENDPOINT sqs get-queue-attributes --queue-url $CANCELLED_URL --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
echo $CREATED_URL > /app/build/created_url.txt
echo $CANCELLED_URL > /app/build/cancelled_url.txt
echo $CREATED_ARN > /app/build/created_arn.txt
echo $CANCELLED_ARN > /app/build/cancelled_arn.txt
echo "Queues created"
echo " CREATED ARN: $CREATED_ARN"
echo " CANCELLED ARN: $CANCELLED_ARN"bus: arn:aws:events:us-east-1:000000000000:event-bus/order-events http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-created-dlq http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled-dlq http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-created http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled Queues created CREATED ARN: arn:aws:sqs:us-east-1:000000000000:orders-created CANCELLED ARN: arn:aws:sqs:us-east-1:000000000000:orders-cancelled [stdout] bus: arn:aws:events:us-east-1:000000000000:event-bus/order-events http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-created-dlq http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled-dlq http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-created http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled Queues created CREATED ARN: arn:aws:sqs:us-east-1:000000000000:orders-created CANCELLED ARN: arn:aws:sqs:us-east-1:000000000000:orders-cancelled
Apply SQS queue policies allowing EventBridge to send messages
export AWS_PAGER=""
export ENDPOINT=http://localstack:4566
CREATED_URL=$(cat /app/build/created_url.txt)
CANCELLED_URL=$(cat /app/build/cancelled_url.txt)
CREATED_ARN=$(cat /app/build/created_arn.txt)
CANCELLED_ARN=$(cat /app/build/cancelled_arn.txt)
BUS_ARN=$(cat /app/build/bus_arn.txt)
# Build queue policies allowing EventBridge to send
cat > /tmp/created-queue-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowEventBridgeSend",
"Effect": "Allow",
"Principal": {"Service": "events.amazonaws.com"},
"Action": "sqs:SendMessage",
"Resource": "$CREATED_ARN",
"Condition": {"ArnEquals": {"aws:SourceArn": "arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-created"}}
}]
}
EOF
cat > /tmp/cancelled-queue-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowEventBridgeSend",
"Effect": "Allow",
"Principal": {"Service": "events.amazonaws.com"},
"Action": "sqs:SendMessage",
"Resource": "$CANCELLED_ARN",
"Condition": {"ArnEquals": {"aws:SourceArn": "arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-cancelled"}}
}]
}
EOF
POLICY_CREATED=$(jq -Rs . < /tmp/created-queue-policy.json)
POLICY_CANCELLED=$(jq -Rs . < /tmp/cancelled-queue-policy.json)
aws --endpoint-url=$ENDPOINT sqs set-queue-attributes \
--queue-url $CREATED_URL \
--attributes "{\"Policy\": $POLICY_CREATED}"
aws --endpoint-url=$ENDPOINT sqs set-queue-attributes \
--queue-url $CANCELLED_URL \
--attributes "{\"Policy\": $POLICY_CANCELLED}"
echo "queue policies set"queue policies set [stdout] queue policies set
{
"query": "select:TodoWrite",
"max_results": 1
}
, output,
{"type": "tool_reference", "tool_name": "TodoWrite"}
Create EventBridge rules
export AWS_PAGER=""
export ENDPOINT=http://localstack:4566
CREATED_ARN=$(cat /app/build/created_arn.txt)
CANCELLED_ARN=$(cat /app/build/cancelled_arn.txt)
# Rule for OrderCreated
aws --endpoint-url=$ENDPOINT events put-rule \
--name on-order-created \
--event-bus-name order-events \
--event-pattern '{"source":["outbox.relay"],"detail-type":["OrderCreated"]}' \
--state ENABLED \
--query 'RuleArn' --output text
aws --endpoint-url=$ENDPOINT events put-targets \
--rule on-order-created \
--event-bus-name order-events \
--targets "Id=created-q,Arn=$CREATED_ARN"
# Rule for OrderCancelled
aws --endpoint-url=$ENDPOINT events put-rule \
--name on-order-cancelled \
--event-bus-name order-events \
--event-pattern '{"source":["outbox.relay"],"detail-type":["OrderCancelled"]}' \
--state ENABLED \
--query 'RuleArn' --output text
aws --endpoint-url=$ENDPOINT events put-targets \
--rule on-order-cancelled \
--event-bus-name order-events \
--targets "Id=cancelled-q,Arn=$CANCELLED_ARN"
echo "rules created"
aws --endpoint-url=$ENDPOINT events list-rules --event-bus-name order-events --query 'Rules[].[Name,State]' --output textarn:aws:events:us-east-1:000000000000:rule/order-events/on-order-created
{
"FailedEntryCount": 0,
"FailedEntries": []
}
arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-cancelled
{
"FailedEntryCount": 0,
"FailedEntries": []
}
rules created
on-order-created ENABLED
on-order-cancelled ENABLED
[stdout]
arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-created
{
"FailedEntryCount": 0,
"FailedEntries": []
}
arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-cancelled
{
"FailedEntryCount": 0,
"FailedEntries": []
}
rules created
on-order-created ENABLED
on-order-cancelled ENABLEDCreate IAM roles with least-privilege inline policies
export AWS_PAGER=""
export ENDPOINT=http://localstack:4566
KEY_ARN=$(cat /app/build/key_arn.txt)
BUS_ARN=$(cat /app/build/bus_arn.txt)
STREAM_ARN=$(cat /app/build/outbox_stream_arn.txt)
ORDERS_ARN="arn:aws:dynamodb:us-east-1:000000000000:table/orders"
OUTBOX_ARN="arn:aws:dynamodb:us-east-1:000000000000:table/outbox"
DEDUP_ARN="arn:aws:dynamodb:us-east-1:000000000000:table/processed_events"
# Trust policy for lambda
cat > /app/policies/lambda-trust.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}
EOF
# order-api policy: only orders + outbox tables (TransactWriteItems), only its CMK for those tables, only basic logs
cat > /app/policies/order-api-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Logs",
"Effect": "Allow",
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/order-api*"
},
{
"Sid": "TransactionalWrite",
"Effect": "Allow",
"Action": ["dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:GetItem"],
"Resource": ["$ORDERS_ARN", "$OUTBOX_ARN"]
},
{
"Sid": "Transact",
"Effect": "Allow",
"Action": ["dynamodb:TransactWriteItems"],
"Resource": ["$ORDERS_ARN", "$OUTBOX_ARN"]
},
{
"Sid": "KmsForOrdersAndOutbox",
"Effect": "Allow",
"Action": ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey", "kms:DescribeKey"],
"Resource": "$KEY_ARN",
"Condition": {
"StringEquals": {
"kms:ViaService": "dynamodb.us-east-1.amazonaws.com"
}
}
}
]
}
EOF
# outbox-relay policy: read outbox stream, write dedup table, putevents to bus, kms for those, logs
cat > /app/policies/outbox-relay-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Logs",
"Effect": "Allow",
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/outbox-relay*"
},
{
"Sid": "ReadOutboxStream",
"Effect": "Allow",
"Action": [
"dynamodb:DescribeStream",
"dynamodb:GetRecords",
"dynamodb:GetShardIterator",
"dynamodb:ListStreams"
],
"Resource": "$STREAM_ARN"
},
{
"Sid": "DedupTable",
"Effect": "Allow",
"Action": ["dynamodb:PutItem", "dynamodb:GetItem"],
"Resource": "$DEDUP_ARN"
},
{
"Sid": "PublishToBus",
"Effect": "Allow",
"Action": "events:PutEvents",
"Resource": "$BUS_ARN"
},
{
"Sid": "KmsForRelay",
"Effect": "Allow",
"Action": ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey", "kms:DescribeKey"],
"Resource": "$KEY_ARN",
"Condition": {
"StringEquals": {
"kms:ViaService": [
"dynamodb.us-east-1.amazonaws.com",
"events.us-east-1.amazonaws.com"
]
}
}
}
]
}
EOF
# Create roles
aws --endpoint-url=$ENDPOINT iam create-role \
--role-name order-api-role \
--assume-role-policy-document file:///app/policies/lambda-trust.json \
--query 'Role.Arn' --output text > /app/build/order_api_role_arn.txt
aws --endpoint-url=$ENDPOINT iam create-role \
--role-name outbox-relay-role \
--assume-role-policy-document file:///app/policies/lambda-trust.json \
--query 'Role.Arn' --output text > /app/build/outbox_relay_role_arn.txt
# Attach inline policies
aws --endpoint-url=$ENDPOINT iam put-role-policy \
--role-name order-api-role \
--policy-name order-api-inline \
--policy-document file:///app/policies/order-api-policy.json
aws --endpoint-url=$ENDPOINT iam put-role-policy \
--role-name outbox-relay-role \
--policy-name outbox-relay-inline \
--policy-document file:///app/policies/outbox-relay-policy.json
echo "order-api-role: $(cat /app/build/order_api_role_arn.txt)"
echo "outbox-relay-role: $(cat /app/build/outbox_relay_role_arn.txt)"order-api-role: arn:aws:iam::000000000000:role/order-api-role outbox-relay-role: arn:aws:iam::000000000000:role/outbox-relay-role [stdout] order-api-role: arn:aws:iam::000000000000:role/order-api-role outbox-relay-role: arn:aws:iam::000000000000:role/outbox-relay-role
Apply KMS key policy
export AWS_PAGER=""
export ENDPOINT=http://localstack:4566
KEY_ID=$(cat /app/build/key_id.txt)
# Update the KMS key policy: allow root + the two lambda roles + dynamodb / events services
cat > /app/policies/key-policy.json <<'EOF'
{
"Version": "2012-10-17",
"Id": "outbox-cmk",
"Statement": [
{
"Sid": "EnableRootAccount",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::000000000000:root"},
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "AllowOrderApiUse",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::000000000000:role/order-api-role"},
"Action": ["kms:Encrypt","kms:Decrypt","kms:GenerateDataKey","kms:DescribeKey"],
"Resource": "*"
},
{
"Sid": "AllowRelayUse",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::000000000000:role/outbox-relay-role"},
"Action": ["kms:Encrypt","kms:Decrypt","kms:GenerateDataKey","kms:DescribeKey"],
"Resource": "*"
},
{
"Sid": "AllowAwsServices",
"Effect": "Allow",
"Principal": {"Service": ["dynamodb.amazonaws.com","events.amazonaws.com","sqs.amazonaws.com","lambda.amazonaws.com"]},
"Action": ["kms:Encrypt","kms:Decrypt","kms:GenerateDataKey","kms:DescribeKey","kms:CreateGrant"],
"Resource": "*"
}
]
}
EOF
aws --endpoint-url=$ENDPOINT kms put-key-policy \
--key-id $KEY_ID \
--policy-name default \
--policy file:///app/policies/key-policy.json
echo "kms key policy applied"kms key policy applied [stdout] kms key policy applied
import json
import os
import time
import uuid
import boto3
ENDPOINT = os.environ.get("AWS_ENDPOINT_URL") or os.environ.get("LOCALSTACK_ENDPOINT")
ddb = boto3.client("dynamodb", endpoint_url=ENDPOINT)
ALLOWED_KINDS = {"OrderCreated", "OrderCancelled"}
def lambda_handler(event, context):
body = event
if isinstance(event, dict) and "body" in event and isinstance(event["body"], str):
body = json.loads(event["body"])
order_id = body["order_id"]
kind = body["kind"]
if kind not in ALLOWED_KINDS:
return {"statusCode": 400, "body": json.dumps({"error": f"unknown kind {kind}"})}
payload = {k: v for k, v in body.items() if k not in ("order_id", "kind")}
event_id = str(uuid.uuid4())
now = int(time.time())
order_item = {
"order_id": {"S": order_id},
"kind": {"S": kind},
"updated_at": {"N": str(now)},
"payload": {"S": json.dumps(payload)},
}
outbox_item = {
"event_id": {"S": event_id},
"order_id": {"S": order_id},
"kind": {"S": kind},
"created_at": {"N": str(now)},
"detail": {"S": json.dumps({"order_id": order_id, "kind": kind, **payload})},
}
ddb.transact_write_items(
TransactItems=[
{"Put": {"TableName": "orders", "Item": order_item}},
{"Put": {"TableName": "outbox", "Item": outbox_item}},
]
)
return {
"statusCode": 200,
"body": json.dumps({"order_id": order_id, "event_id": event_id, "kind": kind}),
}
import json
import os
import time
import boto3
from botocore.exceptions import ClientError
ENDPOINT = os.environ.get("AWS_ENDPOINT_URL") or os.environ.get("LOCALSTACK_ENDPOINT")
events = boto3.client("events", endpoint_url=ENDPOINT)
ddb = boto3.client("dynamodb", endpoint_url=ENDPOINT)
BUS_NAME = os.environ["BUS_NAME"]
DEDUP_TABLE = os.environ["DEDUP_TABLE"]
DEDUP_TTL_SECONDS = int(os.environ.get("DEDUP_TTL_SECONDS", "604800"))
def _claim(event_id):
ttl = int(time.time()) + DEDUP_TTL_SECONDS
try:
ddb.put_item(
TableName=DEDUP_TABLE,
Item={"event_id": {"S": event_id}, "ttl": {"N": str(ttl)}},
ConditionExpression="attribute_not_exists(event_id)",
)
return True
except ClientError as e:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
return False
raise
def lambda_handler(event, context):
failures = []
records = event.get("Records", [])
for r in records:
seq = r.get("dynamodb", {}).get("SequenceNumber")
try:
if r.get("eventName") != "INSERT":
continue
new_image = r["dynamodb"].get("NewImage") or {}
event_id = new_image.get("event_id", {}).get("S")
kind = new_image.get("kind", {}).get("S")
detail_s = new_image.get("detail", {}).get("S", "{}")
if not event_id or kind not in ("OrderCreated", "OrderCancelled"):
continue
if not _claim(event_id):
continue
events.put_events(
Entries=[
{
"EventBusName": BUS_NAME,
"Source": "outbox.relay",
"DetailType": kind,
"Detail": detail_s,
}
]
)
except Exception as exc: # noqa: BLE001
print(f"failed record seq={seq}: {exc}")
if seq:
failures.append({"itemIdentifier": seq})
return {"batchItemFailures": failures}
Package lambda zips
cd /app/lambdas/order-api && zip -q -r /app/build/order-api.zip handler.py cd /app/lambdas/outbox-relay && zip -q -r /app/build/outbox-relay.zip handler.py ls -la /app/build/
total 64 drwxr-xr-x 2 root root 4096 Apr 28 14:27 . drwxr-xr-x 1 root root 4096 Apr 28 14:25 .. -rw-r--r-- 1 root root 61 Apr 28 14:26 bus_arn.txt -rw-r--r-- 1 root root 52 Apr 28 14:26 cancelled_arn.txt -rw-r--r-- 1 root root 56 Apr 28 14:26 cancelled_dlq_arn.txt -rw-r--r-- 1 root root 83 Apr 28 14:26 cancelled_url.txt -rw-r--r-- 1 root root 50 Apr 28 14:26 created_arn.txt -rw-r--r-- 1 root root 54 Apr 28 14:26 created_dlq_arn.txt -rw-r--r-- 1 root root 81 Apr 28 14:26 created_url.txt -rw-r--r-- 1 root root 76 Apr 28 14:25 key_arn.txt -rw-r--r-- 1 root root 37 Apr 28 14:25 key_id.txt -rw-r--r-- 1 root root 790 Apr 28 14:27 order-api.zip -rw-r--r-- 1 root root 46 Apr 28 14:27 order_api_role_arn.txt -rw-r--r-- 1 root root 1018 Apr 28 14:27 outbox-relay.zip -rw-r--r-- 1 root root 49 Apr 28 14:27 outbox_relay_role_arn.txt -rw-r--r-- 1 root root 84 Apr 28 14:25 outbox_stream_arn.txt [stdout] total 64 drwxr-xr-x 2 root root 4096 Apr 28 14:27 . drwxr-xr-x 1 root root 4096 Apr 28 14:25 .. -rw-r--r-- 1 root root 61 Apr 28 14:26 bus_arn.txt -rw-r--r-- 1 root root 52 Apr 28 14:26 cancelled_arn.txt -rw-r--r-- 1 root root 56 Apr 28 14:26 cancelled_dlq_arn.txt -rw-r--r-- 1 root root 83 Apr 28 14:26 cancelled_url.txt -rw-r--r-- 1 root root 50 Apr 28 14:26 created_arn.txt -rw-r--r-- 1 root root 54 Apr 28 14:26 created_dlq_arn.txt -rw-r--r-- 1 root root 81 Apr 28 14:26 created_url.txt -rw-r--r-- 1 root root 76 Apr 28 14:25 key_arn.txt -rw-r--r-- 1 root root 37 Apr 28 14:25 key_id.txt -rw-r--r-- 1 root root 790 Apr 28 14:27 order-api.zip -rw-r--r-- 1 root root 46 Apr 28 14:27 order_api_role_arn.txt -rw-r--r-- 1 root root 1018 Apr 28 14:27 outbox-relay.zip -rw-r--r-- 1 root root 49 Apr 28 14:27 outbox_relay_role_arn.txt -rw-r--r-- 1 root root 84 Apr 28 14:25 outbox_stream_arn.txt
Deploy both lambdas
export AWS_PAGER=""
export ENDPOINT=http://localstack:4566
ORDER_API_ROLE=$(cat /app/build/order_api_role_arn.txt)
RELAY_ROLE=$(cat /app/build/outbox_relay_role_arn.txt)
aws --endpoint-url=$ENDPOINT lambda create-function \
--function-name order-api \
--runtime python3.11 \
--role $ORDER_API_ROLE \
--handler handler.lambda_handler \
--zip-file fileb:///app/build/order-api.zip \
--timeout 15 \
--environment "Variables={AWS_ENDPOINT_URL=http://localstack:4566}" \
--query 'FunctionArn' --output text
aws --endpoint-url=$ENDPOINT lambda create-function \
--function-name outbox-relay \
--runtime python3.11 \
--role $RELAY_ROLE \
--handler handler.lambda_handler \
--zip-file fileb:///app/build/outbox-relay.zip \
--timeout 30 \
--environment "Variables={AWS_ENDPOINT_URL=http://localstack:4566,BUS_NAME=order-events,DEDUP_TABLE=processed_events}" \
--query 'FunctionArn' --output text
# Wait for both
aws --endpoint-url=$ENDPOINT lambda wait function-active --function-name order-api
aws --endpoint-url=$ENDPOINT lambda wait function-active --function-name outbox-relay
echo "lambdas active"arn:aws:lambda:us-east-1:000000000000:function:order-api arn:aws:lambda:us-east-1:000000000000:function:outbox-relay lambdas active [stdout] arn:aws:lambda:us-east-1:000000000000:function:order-api arn:aws:lambda:us-east-1:000000000000:function:outbox-relay lambdas active
Create event source mapping with ReportBatchItemFailures
export AWS_PAGER=""
export ENDPOINT=http://localstack:4566
STREAM_ARN=$(cat /app/build/outbox_stream_arn.txt)
# Create event source mapping with FunctionResponseTypes for ReportBatchItemFailures
aws --endpoint-url=$ENDPOINT lambda create-event-source-mapping \
--function-name outbox-relay \
--event-source-arn $STREAM_ARN \
--starting-position TRIM_HORIZON \
--batch-size 10 \
--function-response-types ReportBatchItemFailures \
--maximum-retry-attempts 3 \
--query '{UUID:UUID,FunctionResponseTypes:FunctionResponseTypes}'
# Wait for it to come up
sleep 3
aws --endpoint-url=$ENDPOINT lambda list-event-source-mappings \
--function-name outbox-relay \
--query 'EventSourceMappings[].[State,FunctionResponseTypes]' --output text{
"UUID": "d5229896-7237-49d2-a579-871fca9aa7bc",
"FunctionResponseTypes": [
"ReportBatchItemFailures"
]
}
Enabled
ReportBatchItemFailures
[stdout]
{
"UUID": "d5229896-7237-49d2-a579-871fca9aa7bc",
"FunctionResponseTypes": [
"ReportBatchItemFailures"
]
}
Enabled
ReportBatchItemFailuresInvoke order-api and verify queues
export AWS_PAGER=""
export ENDPOINT=http://localstack:4566
CREATED_URL=$(cat /app/build/created_url.txt)
CANCELLED_URL=$(cat /app/build/cancelled_url.txt)
# Invoke order-api with OrderCreated for o-1
aws --endpoint-url=$ENDPOINT lambda invoke \
--function-name order-api \
--cli-binary-format raw-in-base64-out \
--payload '{"order_id":"o-1","kind":"OrderCreated","amount":42,"currency":"USD"}' \
/tmp/inv1.json
cat /tmp/inv1.json; echo
# Wait for stream + relay + eventbridge + sqs delivery
echo "waiting 12s for event propagation..."
sleep 12
# Check both queues
echo "== orders-created =="
aws --endpoint-url=$ENDPOINT sqs get-queue-attributes --queue-url $CREATED_URL \
--attribute-names ApproximateNumberOfMessages --query 'Attributes' --output json
echo "== orders-cancelled =="
aws --endpoint-url=$ENDPOINT sqs get-queue-attributes --queue-url $CANCELLED_URL \
--attribute-names ApproximateNumberOfMessages --query 'Attributes' --output json{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
{"statusCode": 200, "body": "{\"order_id\": \"o-1\", \"event_id\": \"51ca6d2a-d77d-45e4-87bf-4129be6ece96\", \"kind\": \"OrderCreated\"}"}
waiting 12s for event propagation...
== orders-created ==
{
"ApproximateNumberOfMessages": "1"
}
== orders-cancelled ==
{
"ApproximateNumberOfMessages": "0"
}
[stdout]
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
{"statusCode": 200, "body": "{\"order_id\": \"o-1\", \"event_id\": \"51ca6d2a-d77d-45e4-87bf-4129be6ece96\", \"kind\": \"OrderCreated\"}"}
waiting 12s for event propagation...
== orders-created ==
{
"ApproximateNumberOfMessages": "1"
}
== orders-cancelled ==
{
"ApproximateNumberOfMessages": "0"
}Verify OrderCreated message and test OrderCancelled
export AWS_PAGER=""
export ENDPOINT=http://localstack:4566
CREATED_URL=$(cat /app/build/created_url.txt)
CANCELLED_URL=$(cat /app/build/cancelled_url.txt)
# Peek at the queued message (don't delete)
echo "== msg in orders-created =="
aws --endpoint-url=$ENDPOINT sqs receive-message --queue-url $CREATED_URL --visibility-timeout 0 --max-number-of-messages 1 --query 'Messages[].Body' --output text | jq .
echo "== processed_events row for o-1 event =="
EVENT_ID=$(echo '51ca6d2a-d77d-45e4-87bf-4129be6ece96')
aws --endpoint-url=$ENDPOINT dynamodb get-item \
--table-name processed_events \
--key "{\"event_id\":{\"S\":\"$EVENT_ID\"}}" --query 'Item' --output json
# Now invoke OrderCancelled with a different order_id
aws --endpoint-url=$ENDPOINT lambda invoke \
--function-name order-api \
--cli-binary-format raw-in-base64-out \
--payload '{"order_id":"o-2","kind":"OrderCancelled","reason":"customer requested"}' \
/tmp/inv2.json
cat /tmp/inv2.json; echo
sleep 12
echo "== orders-created =="
aws --endpoint-url=$ENDPOINT sqs get-queue-attributes --queue-url $CREATED_URL \
--attribute-names ApproximateNumberOfMessages --query 'Attributes' --output json
echo "== orders-cancelled =="
aws --endpoint-url=$ENDPOINT sqs get-queue-attributes --queue-url $CANCELLED_URL \
--attribute-names ApproximateNumberOfMessages --query 'Attributes' --output json
echo "== msg in orders-cancelled =="
aws --endpoint-url=$ENDPOINT sqs receive-message --queue-url $CANCELLED_URL --visibility-timeout 0 --max-number-of-messages 1 --query 'Messages[].Body' --output text | jq .== msg in orders-created ==
{
"version": "0",
"id": "22a09908-db69-4e6d-a665-c3fd83dcc7e5",
"detail-type": "OrderCreated",
"source": "outbox.relay",
"account": "000000000000",
"time": "2026-04-28T14:28:36Z",
"region": "us-east-1",
"resources": [],
"detail": {
"order_id": "o-1",
"kind": "OrderCreated",
"amount": 42,
"currency": "USD"
}
}
== processed_events row for o-1 event ==
{
"ttl": {
"N": "1777991316"
},
"event_id": {
"S": "51ca6d2a-d77d-45e4-87bf-4129be6ece96"
}
}
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
{"statusCode": 200, "body": "{\"order_id\": \"o-2\", \"event_id\": \"1ddf7e89-a471-4d89-99b2-a70c0d1c5ab0\", \"kind\": \"OrderCancelled\"}"}
== orders-created ==
{
"ApproximateNumberOfMessages": "1"
}
== orders-cancelled ==
{
"ApproximateNumberOfMessages": "1"
}
== msg in orders-cancelled ==
{
"version": "0",
"id": "c6c4abed-dc32-4328-96b0-4ce1839ae515",
"detail-type": "OrderCancelled",
"source": "outbox.relay",
"account": "000000000000",
"time": "2026-04-28T14:29:03Z",
"region": "us-east-1",
"resources": [],
"detail": {
"order_id": "o-2",
"kind": "OrderCancelled",
"reason": "customer requested"
}
}
[stdout]
== msg in orders-created ==
{
"version": "0",
"id": "22a09908-db69-4e6d-a665-c3fd83dcc7e5",
"detail-type": "OrderCreated",
"source": "outbox.relay",
"account": "000000000000",
"time": "2026-04-28T14:28:36Z",
"region": "us-east-1",
"resources": [],
"detail": {
"order_id": "o-1",
"kind": "OrderCreated",
"amount": 42,
"currency": "USD"
}
}
== processed_events row for o-1 event ==
{
"ttl": {
"N": "1777991316"
},
"event_id": {
"S": "51ca6d2a-d77d-45e4-87bf-4129be6ece96"
}
}
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
{"statusCode": 200, "body": "{\"order_id\": \"o-2\", \"event_id\": \"1ddf7e89-a471-4d89-99b2-a70c0d1c5ab0\", \"kind\": \"OrderCancelled\"}"}
== orders-created ==
{
"ApproximateNumberOfMessages": "1"
}
== orders-cancelled ==
{
"ApproximateNumberOfMessages": "1"
}
== msg in orders-cancelled ==
{
"version": "0",
"id": "c6c4abed-dc32-4328-96b0-4ce1839ae515",
"detail-type": "OrderCancelled",
"source": "outbox.relay",
"account": "000000000000",
"time": "2026-04-28T14:29:03Z",
"region": "us-east-1",
"resources": [],
"detail": {
"order_id": "o-2",
"kind": "OrderCancelled",
"reason": "customer requested"
}
}Print final resource summary and policy docs
export AWS_PAGER=""
export ENDPOINT=http://localstack:4566
echo "======= RESOURCES ======="
aws --endpoint-url=$ENDPOINT kms list-aliases --query "Aliases[?AliasName=='alias/outbox-cmk'].[AliasName,TargetKeyId]" --output text
aws --endpoint-url=$ENDPOINT dynamodb list-tables --query 'TableNames' --output text
aws --endpoint-url=$ENDPOINT events list-event-buses --query "EventBuses[?Name=='order-events'].Name" --output text
aws --endpoint-url=$ENDPOINT events list-rules --event-bus-name order-events --query 'Rules[].Name' --output text
aws --endpoint-url=$ENDPOINT sqs list-queues --query 'QueueUrls' --output text
aws --endpoint-url=$ENDPOINT lambda list-functions --query 'Functions[].FunctionName' --output text
aws --endpoint-url=$ENDPOINT iam list-roles --query "Roles[?starts_with(RoleName,'order-') || starts_with(RoleName,'outbox-')].RoleName" --output text
echo "======= STREAM SETTINGS ======="
aws --endpoint-url=$ENDPOINT dynamodb describe-table --table-name outbox \
--query 'Table.StreamSpecification' --output json
echo "======= ESM SETTINGS ======="
aws --endpoint-url=$ENDPOINT lambda list-event-source-mappings --function-name outbox-relay \
--query 'EventSourceMappings[].{State:State,FunctionResponseTypes:FunctionResponseTypes,EventSource:EventSourceArn}' --output json
echo "======= ORDER-API POLICY ======="
aws --endpoint-url=$ENDPOINT iam get-role-policy --role-name order-api-role --policy-name order-api-inline --query 'PolicyDocument' --output json
echo "======= RELAY POLICY ======="
aws --endpoint-url=$ENDPOINT iam get-role-policy --role-name outbox-relay-role --policy-name outbox-relay-inline --query 'PolicyDocument' --output json======= RESOURCES =======
alias/outbox-cmk a5ae8978-f0fd-4af3-a3fc-80c5235b27fa
orders outbox processed_events
order-events
on-order-created on-order-cancelled
http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-created-dlq http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled-dlq http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-created http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled
order-api outbox-relay
outbox-relay-role order-api-role
======= STREAM SETTINGS =======
{
"StreamEnabled": true,
"StreamViewType": "NEW_AND_OLD_IMAGES"
}
======= ESM SETTINGS =======
[
{
"State": "Enabled",
"FunctionResponseTypes": [
"ReportBatchItemFailures"
],
"EventSource": "arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T14:25:37.799"
}
]
======= ORDER-API POLICY =======
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Logs",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/order-api*"
},
{
"Sid": "TransactionalWrite",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:GetItem"
],
"Resource": [
"arn:aws:dynamodb:us-east-1:000000000000:table/orders",
"arn:aws:dynamodb:us-east-1:000000000000:table/outbox"
]
},
{
"Sid": "Transact",
"Effect": "Allow",
"Action": [
"dynamodb:TransactWriteItems"
],
"Resource": [
"arn:aws:dynamodb:us-east-1:000000000000:table/orders",
"arn:aws:dynamodb:us-east-1:000000000000:table/outbox"
]
},
{
"Sid": "KmsForOrdersAndOutbox",
"Effect": "Allow",
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "arn:aws:kms:us-east-1:000000000000:key/a5ae8978-f0fd-4af3-a3fc-80c5235b27fa",
"Condition": {
"StringEquals": {
"kms:ViaService": "dynamodb.us-east-1.amazonaws.com"
}
}
}
]
}
======= RELAY POLICY =======
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Logs",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/outbox-relay*"
},
{
"Sid": "ReadOutboxStream",
"Effect": "Allow",
"Action": [
"dynamodb:DescribeStream",
"dynamodb:GetRecords",
"dynamodb:GetShardIterator",
"dynamodb:ListStreams"
],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T14:25:37.799"
},
{
"Sid": "DedupTable",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/processed_events"
},
{
"Sid": "PublishToBus",
"Effect": "Allow",
"Action": "events:PutEvents",
"Resource": "arn:aws:events:us-east-1:000000000000:event-bus/order-events"
},
{
"Sid": "KmsForRelay",
"Effect": "Allow",
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "arn:aws:kms:us-east-1:000000000000:key/a5ae8978-f0fd-4af3-a3fc-80c5235b27fa",
"Condition": {
"StringEquals": {
"kms:ViaService": [
"dynamodb.us-east-1.amazonaws.com",
"events.us-east-1.amazonaws.com"
]
}
}
}
]
}
[stdout]
======= RESOURCES =======
alias/outbox-cmk a5ae8978-f0fd-4af3-a3fc-80c5235b27fa
orders outbox processed_events
order-events
on-order-created on-order-cancelled
http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-created-dlq http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled-dlq http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-created http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled
order-api outbox-relay
outbox-relay-role order-api-role
======= STREAM SETTINGS =======
{
"StreamEnabled": true,
"StreamViewType": "NEW_AND_OLD_IMAGES"
}
======= ESM SETTINGS =======
[
{
"State": "Enabled",
"FunctionResponseTypes": [
"ReportBatchItemFailures"
],
"EventSource": "arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T14:25:37.799"
}
]
======= ORDER-API POLICY =======
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Logs",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/order-api*"
},
{
"Sid": "TransactionalWrite",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:GetItem"
],
"Resource": [
"arn:aws:dynamodb:us-east-1:000000000000:table/orders",
"arn:aws:dynamodb:us-east-1:000000000000:table/outbox"
]
},
{
"Sid": "Transact",
"Effect": "Allow",
"Action": [
"dynamodb:TransactWriteItems"
],
"Resource": [
"arn:aws:dynamodb:us-east-1:000000000000:table/orders",
"arn:aws:dynamodb:us-east-1:000000000000:table/outbox"
]
},
{
"Sid": "KmsForOrdersAndOutbox",
"Effect": "Allow",
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "arn:aws:kms:us-east-1:000000000000:key/a5ae8978-f0fd-4af3-a3fc-80c5235b27fa",
"Condition": {
"StringEquals": {
"kms:ViaService": "dynamodb.us-east-1.amazonaws.com"
}
}
}
]
}
======= RELAY POLICY =======
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Logs",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/outbox-relay*"
},
{
"Sid": "ReadOutboxStream",
"Effect": "Allow",
"Action": [
"dynamodb:DescribeStream",
"dynamodb:GetRecords",
"dynamodb:GetShardIterator",
"dynamodb:ListStreams"
],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T14:25:37.799"
},
{
"Sid": "DedupTable",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/processed_events"
},
{
"Sid": "PublishToBus",
"Effect": "Allow",
"Action": "events:PutEvents",
"Resource": "arn:aws:events:us-east-1:000000000000:event-bus/order-events"
},
{
"Sid": "KmsForRelay",
"Effect": "Allow",
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "arn:aws:kms:us-east-1:000000000000:key/a5ae8978-f0fd-4af3-a3fc-80c5235b27fa",
"Condition": {
"StringEquals": {
"kms:ViaService": [
"dynamodb.us-east-1.amazonaws.com",
"events.us-east-1.amazonaws.com"
]
}
}
}
]
}No file changes, the agent left the workspace untouched.
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-8.4.1, pluggy-1.6.0 -- /root/.cache/uv/archive-v0/5n5j6otN-n7Sh_-4AwrjP/bin/python
cachedir: .pytest_cache
rootdir: /tests
plugins: json-ctrf-0.3.5
collecting ... collected 20 items
../tests/test_outputs.py::test_01_cmk_alias_exists_and_enabled PASSED [ 5%]
../tests/test_outputs.py::test_02_orders_table_exists PASSED [ 10%]
../tests/test_outputs.py::test_03_outbox_table_exists PASSED [ 15%]
../tests/test_outputs.py::test_04_processed_events_table_exists PASSED [ 20%]
../tests/test_outputs.py::test_05_custom_event_bus_exists PASSED [ 25%]
../tests/test_outputs.py::test_06_orders_table_encrypted_with_cmk PASSED [ 30%]
../tests/test_outputs.py::test_07_outbox_table_encrypted_with_cmk PASSED [ 35%]
../tests/test_outputs.py::test_08_outbox_stream_is_new_and_old_images PASSED [ 40%]
../tests/test_outputs.py::test_09_processed_events_ttl_enabled PASSED [ 45%]
../tests/test_outputs.py::test_10_both_rule_patterns_use_array_wrap_and_disjoint_types PASSED [ 50%]
../tests/test_outputs.py::test_11_each_rule_targets_only_its_intended_queue PASSED [ 55%]
../tests/test_outputs.py::test_12_both_main_queues_encrypted_with_cmk PASSED [ 60%]
../tests/test_outputs.py::test_13_main_queues_have_redrive_to_distinct_dlqs PASSED [ 65%]
../tests/test_outputs.py::test_14_queue_policies_scope_events_with_source_arn PASSED [ 70%]
../tests/test_outputs.py::test_15_relay_esm_has_batch_item_failures_on_outbox_stream PASSED [ 75%]
../tests/test_outputs.py::test_16_relay_role_is_scoped_not_wildcard PASSED [ 80%]
../tests/test_outputs.py::test_17_order_api_role_scoped_to_orders_and_outbox_only PASSED [ 85%]
../tests/test_outputs.py::test_18_e2e_order_created_routes_to_created_queue_only PASSED [ 90%]
../tests/test_outputs.py::test_19_e2e_order_cancelled_routes_to_cancelled_queue_only PASSED [ 95%]
../tests/test_outputs.py::test_20_e2e_idempotent_duplicate_does_not_double_fanout PASSED [100%]
=============================== warnings summary ===============================
test_outputs.py: 90 warnings
/root/.cache/uv/archive-v0/5n5j6otN-n7Sh_-4AwrjP/lib/python3.12/site-packages/botocore/auth.py:424: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
datetime_now = datetime.datetime.utcnow()
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
==================================== PASSES ====================================
=========================== short test summary info ============================
PASSED ../tests/test_outputs.py::test_01_cmk_alias_exists_and_enabled
PASSED ../tests/test_outputs.py::test_02_orders_table_exists
PASSED ../tests/test_outputs.py::test_03_outbox_table_exists
PASSED ../tests/test_outputs.py::test_04_processed_events_table_exists
PASSED ../tests/test_outputs.py::test_05_custom_event_bus_exists
PASSED ../tests/test_outputs.py::test_06_orders_table_encrypted_with_cmk
PASSED ../tests/test_outputs.py::test_07_outbox_table_encrypted_with_cmk
PASSED ../tests/test_outputs.py::test_08_outbox_stream_is_new_and_old_images
PASSED ../tests/test_outputs.py::test_09_processed_events_ttl_enabled
PASSED ../tests/test_outputs.py::test_10_both_rule_patterns_use_array_wrap_and_disjoint_types
PASSED ../tests/test_outputs.py::test_11_each_rule_targets_only_its_intended_queue
PASSED ../tests/test_outputs.py::test_12_both_main_queues_encrypted_with_cmk
PASSED ../tests/test_outputs.py::test_13_main_queues_have_redrive_to_distinct_dlqs
PASSED ../tests/test_outputs.py::test_14_queue_policies_scope_events_with_source_arn
PASSED ../tests/test_outputs.py::test_15_relay_esm_has_batch_item_failures_on_outbox_stream
PASSED ../tests/test_outputs.py::test_16_relay_role_is_scoped_not_wildcard
PASSED ../tests/test_outputs.py::test_17_order_api_role_scoped_to_orders_and_outbox_only
PASSED ../tests/test_outputs.py::test_18_e2e_order_created_routes_to_created_queue_only
PASSED ../tests/test_outputs.py::test_19_e2e_order_cancelled_routes_to_cancelled_queue_only
PASSED ../tests/test_outputs.py::test_20_e2e_idempotent_duplicate_does_not_double_fanout
======================= 20 passed, 90 warnings in 21.99s =======================Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_ce7b35ada36b4c4a. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_ce7b35ada36b4c4a · verifier authoritative; classifier explanatory.