tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.Check working dir and tools
ls /app && which aws python3 zip jq
/usr/local/bin/aws /opt/venv/bin/python3 /usr/bin/zip /usr/bin/jq [stdout] /usr/local/bin/aws /opt/venv/bin/python3 /usr/bin/zip /usr/bin/jq
Check app dir and AWS env
ls -la /app && env | grep -i aws
total 8 drwxr-xr-x 2 root root 4096 Apr 21 20:05 . drwxr-xr-x 1 root root 4096 Apr 28 14:52 .. AWS_DEFAULT_REGION=us-east-1 AWS_PAGER= AWS_SECRET_ACCESS_KEY=test AWS_ACCESS_KEY_ID=test AWS_ENDPOINT_URL=http://localstack:4566 [stdout] total 8 drwxr-xr-x 2 root root 4096 Apr 21 20:05 . drwxr-xr-x 1 root root 4096 Apr 28 14:52 .. AWS_DEFAULT_REGION=us-east-1 AWS_PAGER= AWS_SECRET_ACCESS_KEY=test AWS_ACCESS_KEY_ID=test AWS_ENDPOINT_URL=http://localstack:4566
Verify localstack reachable
aws sts get-caller-identity && curl -s http://localstack:4566/_localstack/health | jq -r '.version, .services.kms, .services.dynamodb, .services.lambda, .services.events, .services.sqs, .services.iam'
{
"UserId": "AKIAIOSFODNN7EXAMPLE",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
4.13.1
available
available
available
available
available
available
[stdout]
{
"UserId": "AKIAIOSFODNN7EXAMPLE",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
4.13.1
available
available
available
available
available
availableCreate dirs
mkdir -p /app/lambdas /app/policies /app/build
(Bash completed with no output)
import json
import os
import uuid
from datetime import datetime, timezone
import boto3
ENDPOINT = os.environ.get("LOCALSTACK_ENDPOINT", "http://localstack:4566")
ORDERS_TABLE = os.environ["ORDERS_TABLE"]
OUTBOX_TABLE = os.environ["OUTBOX_TABLE"]
_ddb = boto3.client("dynamodb", endpoint_url=ENDPOINT, region_name=os.environ.get("AWS_REGION", "us-east-1"))
_ALLOWED_KINDS = ("OrderCreated", "OrderCancelled")
def _to_attr(value):
if isinstance(value, bool):
return {"BOOL": value}
if isinstance(value, (int, float)):
return {"N": str(value)}
if value is None:
return {"NULL": True}
return {"S": str(value)}
def handler(event, _ctx):
body = event if isinstance(event, dict) else json.loads(event)
order_id = body.get("order_id")
kind = body.get("kind")
if not order_id or kind not in _ALLOWED_KINDS:
return {"statusCode": 400, "error": "order_id required and kind must be OrderCreated or OrderCancelled"}
event_id = str(uuid.uuid4())
now = datetime.now(timezone.utc).isoformat()
order_item = {
"order_id": {"S": order_id},
"kind": {"S": kind},
"updated_at": {"S": now},
}
for k, v in body.items():
if k in ("order_id", "kind"):
continue
order_item[k] = _to_attr(v)
outbox_item = {
"event_id": {"S": event_id},
"order_id": {"S": order_id},
"kind": {"S": kind},
"created_at": {"S": now},
"payload": {"S": json.dumps(body)},
}
_ddb.transact_write_items(
TransactItems=[
{"Put": {"TableName": ORDERS_TABLE, "Item": order_item}},
{"Put": {"TableName": OUTBOX_TABLE, "Item": outbox_item}},
]
)
return {"statusCode": 200, "order_id": order_id, "event_id": event_id, "kind": kind}
import json
import os
from datetime import datetime, timezone
import boto3
from botocore.exceptions import ClientError
ENDPOINT = os.environ.get("LOCALSTACK_ENDPOINT", "http://localstack:4566")
BUS_NAME = os.environ["BUS_NAME"]
DEDUP_TABLE = os.environ["DEDUP_TABLE"]
DEDUP_TTL_SECONDS = int(os.environ.get("DEDUP_TTL_SECONDS", str(7 * 24 * 3600)))
_region = os.environ.get("AWS_REGION", "us-east-1")
_events = boto3.client("events", endpoint_url=ENDPOINT, region_name=_region)
_ddb = boto3.client("dynamodb", endpoint_url=ENDPOINT, region_name=_region)
def _claim(event_id):
expires = int(datetime.now(timezone.utc).timestamp()) + DEDUP_TTL_SECONDS
try:
_ddb.put_item(
TableName=DEDUP_TABLE,
Item={"event_id": {"S": event_id}, "ttl": {"N": str(expires)}},
ConditionExpression="attribute_not_exists(event_id)",
)
return True
except ClientError as e:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
return False
raise
def _release(event_id):
try:
_ddb.delete_item(TableName=DEDUP_TABLE, Key={"event_id": {"S": event_id}})
except ClientError:
pass
def handler(event, _ctx):
failures = []
for rec in event.get("Records", []):
seq = rec.get("dynamodb", {}).get("SequenceNumber")
try:
if rec.get("eventName") != "INSERT":
continue
new_image = rec["dynamodb"].get("NewImage") or {}
event_id = new_image.get("event_id", {}).get("S")
kind = new_image.get("kind", {}).get("S")
payload_raw = new_image.get("payload", {}).get("S", "{}")
if not event_id or kind not in ("OrderCreated", "OrderCancelled"):
continue
if not _claim(event_id):
continue
try:
detail = json.loads(payload_raw)
detail["event_id"] = event_id
_events.put_events(
Entries=[
{
"Source": "outbox.relay",
"DetailType": kind,
"Detail": json.dumps(detail),
"EventBusName": BUS_NAME,
}
]
)
except Exception:
_release(event_id)
raise
except Exception as e:
print(f"relay failure seq={seq}: {e}")
if seq:
failures.append({"itemIdentifier": seq})
return {"batchItemFailures": failures}
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "lambda.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}
{
"Version": "2012-10-17",
"Id": "outbox-cmk-policy",
"Statement": [
{
"Sid": "RootAccountFull",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::000000000000:root" },
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "AllowDynamoDBSSE",
"Effect": "Allow",
"Principal": { "Service": "dynamodb.amazonaws.com" },
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey",
"kms:CreateGrant"
],
"Resource": "*"
},
{
"Sid": "AllowSQSSSE",
"Effect": "Allow",
"Principal": { "Service": "sqs.amazonaws.com" },
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
],
"Resource": "*"
},
{
"Sid": "AllowEventBridgeDeliverToSQS",
"Effect": "Allow",
"Principal": { "Service": "events.amazonaws.com" },
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "*"
}
]
}
{
"query": "select:TodoWrite",
"max_results": 1
}
, output,
{"type": "tool_reference", "tool_name": "TodoWrite"}
Create CMK + alias
KEY_ID=$(aws kms create-key --policy file:///app/policies/cmk-policy.json --description "outbox cmk" --query 'KeyMetadata.KeyId' --output text) && echo "KEY_ID=$KEY_ID" && aws kms create-alias --alias-name alias/outbox-cmk --target-key-id "$KEY_ID" && KEY_ARN=$(aws kms describe-key --key-id alias/outbox-cmk --query 'KeyMetadata.Arn' --output text) && echo "KEY_ARN=$KEY_ARN" && echo "$KEY_ID" > /app/build/key_id && echo "$KEY_ARN" > /app/build/key_arn
KEY_ID=d1e5c8d8-871d-4ae3-bfc5-890beb11dae3 KEY_ARN=arn:aws:kms:us-east-1:000000000000:key/d1e5c8d8-871d-4ae3-bfc5-890beb11dae3 [stdout] KEY_ID=d1e5c8d8-871d-4ae3-bfc5-890beb11dae3 KEY_ARN=arn:aws:kms:us-east-1:000000000000:key/d1e5c8d8-871d-4ae3-bfc5-890beb11dae3
Create DDB tables
KEY_ID=$(cat /app/build/key_id) aws 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=alias/outbox-cmk" \ --query 'TableDescription.TableArn' --output text aws 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 \ --sse-specification "Enabled=true,SSEType=KMS,KMSMasterKeyId=alias/outbox-cmk" \ --stream-specification "StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES" \ --query 'TableDescription.TableArn' --output text aws 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=alias/outbox-cmk" \ --query 'TableDescription.TableArn' --output text
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 [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
Enable TTL, capture stream ARN
aws dynamodb update-time-to-live --table-name processed_events --time-to-live-specification "Enabled=true,AttributeName=ttl" --query 'TimeToLiveSpecification' --output json OUTBOX_STREAM_ARN=$(aws dynamodb describe-table --table-name outbox --query 'Table.LatestStreamArn' --output text) echo "OUTBOX_STREAM_ARN=$OUTBOX_STREAM_ARN" echo "$OUTBOX_STREAM_ARN" > /app/build/outbox_stream_arn ORDERS_STREAM=$(aws dynamodb describe-table --table-name orders --query 'Table.LatestStreamArn' --output text) echo "ORDERS_STREAM=$ORDERS_STREAM (should be None)"
{
"Enabled": true,
"AttributeName": "ttl"
}
OUTBOX_STREAM_ARN=arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T14:57:25.643
ORDERS_STREAM=None (should be None)
[stdout]
{
"Enabled": true,
"AttributeName": "ttl"
}
OUTBOX_STREAM_ARN=arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T14:57:25.643
ORDERS_STREAM=None (should be None)Create event bus
KEY_ARN=$(cat /app/build/key_arn) aws events create-event-bus --name order-events --kms-key-identifier "$KEY_ARN" --query 'EventBusArn' --output text 2>&1 | tee /tmp/bus.out BUS_ARN=$(aws events describe-event-bus --name order-events --query 'Arn' --output text) echo "BUS_ARN=$BUS_ARN" echo "$BUS_ARN" > /app/build/bus_arn
arn:aws:events:us-east-1:000000000000:event-bus/order-events BUS_ARN=arn:aws:events:us-east-1:000000000000:event-bus/order-events [stdout] arn:aws:events:us-east-1:000000000000:event-bus/order-events BUS_ARN=arn:aws:events:us-east-1:000000000000:event-bus/order-events
Create DLQs + main queues with CMK
KEY_ID=$(cat /app/build/key_id)
# DLQs first (no redrive policy)
for q in orders-created-dlq orders-cancelled-dlq; do
aws sqs create-queue --queue-name "$q" \
--attributes "{\"KmsMasterKeyId\":\"$KEY_ID\",\"KmsDataKeyReusePeriodSeconds\":\"300\"}" \
--query 'QueueUrl' --output text
done
# Get DLQ ARNs
CREATED_DLQ_URL=$(aws sqs get-queue-url --queue-name orders-created-dlq --query 'QueueUrl' --output text)
CREATED_DLQ_ARN=$(aws sqs get-queue-attributes --queue-url "$CREATED_DLQ_URL" --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
CANCELLED_DLQ_URL=$(aws sqs get-queue-url --queue-name orders-cancelled-dlq --query 'QueueUrl' --output text)
CANCELLED_DLQ_ARN=$(aws sqs get-queue-attributes --queue-url "$CANCELLED_DLQ_URL" --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
echo "CREATED_DLQ_ARN=$CREATED_DLQ_ARN"
echo "CANCELLED_DLQ_ARN=$CANCELLED_DLQ_ARN"
echo "$CREATED_DLQ_ARN" > /app/build/created_dlq_arn
echo "$CANCELLED_DLQ_ARN" > /app/build/cancelled_dlq_arn
# Now main queues with redrive
aws sqs create-queue --queue-name orders-created \
--attributes "{\"KmsMasterKeyId\":\"$KEY_ID\",\"KmsDataKeyReusePeriodSeconds\":\"300\",\"RedrivePolicy\":\"{\\\"deadLetterTargetArn\\\":\\\"$CREATED_DLQ_ARN\\\",\\\"maxReceiveCount\\\":\\\"5\\\"}\"}" \
--query 'QueueUrl' --output text
aws sqs create-queue --queue-name orders-cancelled \
--attributes "{\"KmsMasterKeyId\":\"$KEY_ID\",\"KmsDataKeyReusePeriodSeconds\":\"300\",\"RedrivePolicy\":\"{\\\"deadLetterTargetArn\\\":\\\"$CANCELLED_DLQ_ARN\\\",\\\"maxReceiveCount\\\":\\\"5\\\"}\"}" \
--query 'QueueUrl' --output text
CREATED_URL=$(aws sqs get-queue-url --queue-name orders-created --query 'QueueUrl' --output text)
CREATED_ARN=$(aws sqs get-queue-attributes --queue-url "$CREATED_URL" --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
CANCELLED_URL=$(aws sqs get-queue-url --queue-name orders-cancelled --query 'QueueUrl' --output text)
CANCELLED_ARN=$(aws sqs get-queue-attributes --queue-url "$CANCELLED_URL" --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
echo "CREATED_ARN=$CREATED_ARN"
echo "CANCELLED_ARN=$CANCELLED_ARN"
echo "$CREATED_URL" > /app/build/created_url
echo "$CANCELLED_URL" > /app/build/cancelled_url
echo "$CREATED_ARN" > /app/build/created_arn
echo "$CANCELLED_ARN" > /app/build/cancelled_arnhttp://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 CREATED_DLQ_ARN=arn:aws:sqs:us-east-1:000000000000:orders-created-dlq CANCELLED_DLQ_ARN=arn:aws:sqs:us-east-1: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 CREATED_ARN=arn:aws:sqs:us-east-1:000000000000:orders-created CANCELLED_ARN=arn:aws:sqs:us-east-1:000000000000:orders-cancelled [stdout] 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 CREATED_DLQ_ARN=arn:aws:sqs:us-east-1:000000000000:orders-created-dlq CANCELLED_DLQ_ARN=arn:aws:sqs:us-east-1: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 CREATED_ARN=arn:aws:sqs:us-east-1:000000000000:orders-created CANCELLED_ARN=arn:aws:sqs:us-east-1:000000000000:orders-cancelled
Create EB rules and targets
CREATED_ARN=$(cat /app/build/created_arn)
CANCELLED_ARN=$(cat /app/build/cancelled_arn)
# rules
aws 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 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 events put-targets \
--rule on-order-created \
--event-bus-name order-events \
--targets "Id=created-queue,Arn=$CREATED_ARN" \
--query 'FailedEntryCount' --output text
aws events put-targets \
--rule on-order-cancelled \
--event-bus-name order-events \
--targets "Id=cancelled-queue,Arn=$CANCELLED_ARN" \
--query 'FailedEntryCount' --output textarn:aws:events:us-east-1:000000000000:rule/order-events/on-order-created arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-cancelled 0 0 [stdout] arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-created arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-cancelled 0 0
Attach SQS queue policies for EB
CREATED_ARN=$(cat /app/build/created_arn)
CANCELLED_ARN=$(cat /app/build/cancelled_arn)
CREATED_URL=$(cat /app/build/created_url)
CANCELLED_URL=$(cat /app/build/cancelled_url)
CREATED_RULE_ARN="arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-created"
CANCELLED_RULE_ARN="arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-cancelled"
cat > /tmp/created-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowEBCreatedRule",
"Effect": "Allow",
"Principal": { "Service": "events.amazonaws.com" },
"Action": "sqs:SendMessage",
"Resource": "$CREATED_ARN",
"Condition": { "ArnEquals": { "aws:SourceArn": "$CREATED_RULE_ARN" } }
}
]
}
EOF
cat > /tmp/cancelled-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowEBCancelledRule",
"Effect": "Allow",
"Principal": { "Service": "events.amazonaws.com" },
"Action": "sqs:SendMessage",
"Resource": "$CANCELLED_ARN",
"Condition": { "ArnEquals": { "aws:SourceArn": "$CANCELLED_RULE_ARN" } }
}
]
}
EOF
aws sqs set-queue-attributes --queue-url "$CREATED_URL" --attributes "Policy=$(jq -c . /tmp/created-policy.json)"
aws sqs set-queue-attributes --queue-url "$CANCELLED_URL" --attributes "Policy=$(jq -c . /tmp/cancelled-policy.json)"
echo "queue policies set"aws: [ERROR]: An error occurred (ParamValidation): Error parsing parameter '--attributes': Expected: '=', received: '"' for input:
Policy={"Version":"2012-10-17","Statement":[{"Sid":"AllowEBCreatedRule","Effect":"Allow","Principal":{"Service":"events.amazonaws.com"},"Action":"sqs:SendMessage","Resource":"arn:aws:sqs:us-east-1:000000000000:orders-created","Condition":{"ArnEquals":{"aws:SourceArn":"arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-created"}}}]}
^
aws: [ERROR]: An error occurred (ParamValidation): Error parsing parameter '--attributes': Expected: '=', received: '"' for input:
Policy={"Version":"2012-10-17","Statement":[{"Sid":"AllowEBCancelledRule","Effect":"Allow","Principal":{"Service":"events.amazonaws.com"},"Action":"sqs:SendMessage","Resource":"arn:aws:sqs:us-east-1:000000000000:orders-cancelled","Condition":{"ArnEquals":{"aws:SourceArn":"arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-cancelled"}}}]}
^
queue policies set
[stdout]
aws: [ERROR]: An error occurred (ParamValidation): Error parsing parameter '--attributes': Expected: '=', received: '"' for input:
Policy={"Version":"2012-10-17","Statement":[{"Sid":"AllowEBCreatedRule","Effect":"Allow","Principal":{"Service":"events.amazonaws.com"},"Action":"sqs:SendMessage","Resource":"arn:aws:sqs:us-east-1:000000000000:orders-created","Condition":{"ArnEquals":{"aws:SourceArn":"arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-created"}}}]}
^
aws: [ERROR]: An error occurred (ParamValidation): Error parsing parameter '--attributes': Expected: '=', received: '"' for input:
Policy={"Version":"2012-10-17","Statement":[{"Sid":"AllowEBCancelledRule","Effect":"Allow","Principal":{"Service":"events.amazonaws.com"},"Action":"sqs:SendMessage","Resource":"arn:aws:sqs:us-east-1:000000000000:orders-cancelled","Condition":{"ArnEquals":{"aws:SourceArn":"arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-cancelled"}}}]}
^
queue policies setSet queue policies via file
CREATED_URL=$(cat /app/build/created_url)
CANCELLED_URL=$(cat /app/build/cancelled_url)
POL_CREATED=$(jq -c . /tmp/created-policy.json)
POL_CANCELLED=$(jq -c . /tmp/cancelled-policy.json)
jq -n --arg p "$POL_CREATED" '{Policy:$p}' > /tmp/sqs-attrs-created.json
jq -n --arg p "$POL_CANCELLED" '{Policy:$p}' > /tmp/sqs-attrs-cancelled.json
aws sqs set-queue-attributes --queue-url "$CREATED_URL" --attributes file:///tmp/sqs-attrs-created.json
aws sqs set-queue-attributes --queue-url "$CANCELLED_URL" --attributes file:///tmp/sqs-attrs-cancelled.json
echo "verifying:"
aws sqs get-queue-attributes --queue-url "$CREATED_URL" --attribute-names Policy --query 'Attributes.Policy' --output text | jq -c .
aws sqs get-queue-attributes --queue-url "$CANCELLED_URL" --attribute-names Policy --query 'Attributes.Policy' --output text | jq -c .verifying:
{"Version":"2012-10-17","Statement":[{"Sid":"AllowEBCreatedRule","Effect":"Allow","Principal":{"Service":"events.amazonaws.com"},"Action":"sqs:SendMessage","Resource":"arn:aws:sqs:us-east-1:000000000000:orders-created","Condition":{"ArnEquals":{"aws:SourceArn":"arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-created"}}}]}
{"Version":"2012-10-17","Statement":[{"Sid":"AllowEBCancelledRule","Effect":"Allow","Principal":{"Service":"events.amazonaws.com"},"Action":"sqs:SendMessage","Resource":"arn:aws:sqs:us-east-1:000000000000:orders-cancelled","Condition":{"ArnEquals":{"aws:SourceArn":"arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-cancelled"}}}]}
[stdout]
verifying:
{"Version":"2012-10-17","Statement":[{"Sid":"AllowEBCreatedRule","Effect":"Allow","Principal":{"Service":"events.amazonaws.com"},"Action":"sqs:SendMessage","Resource":"arn:aws:sqs:us-east-1:000000000000:orders-created","Condition":{"ArnEquals":{"aws:SourceArn":"arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-created"}}}]}
{"Version":"2012-10-17","Statement":[{"Sid":"AllowEBCancelledRule","Effect":"Allow","Principal":{"Service":"events.amazonaws.com"},"Action":"sqs:SendMessage","Resource":"arn:aws:sqs:us-east-1:000000000000:orders-cancelled","Condition":{"ArnEquals":{"aws:SourceArn":"arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-cancelled"}}}]}{
"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": "OrdersAndOutboxTransactWrite",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DescribeTable"
],
"Resource": [
"arn:aws:dynamodb:us-east-1:000000000000:table/orders",
"arn:aws:dynamodb:us-east-1:000000000000:table/outbox"
]
},
{
"Sid": "TransactWrite",
"Effect": "Allow",
"Action": "dynamodb:TransactWriteItems",
"Resource": [
"arn:aws:dynamodb:us-east-1:000000000000:table/orders",
"arn:aws:dynamodb:us-east-1:000000000000:table/outbox"
]
}
]
}
Write outbox-relay policy
OUTBOX_STREAM_ARN=$(cat /app/build/outbox_stream_arn)
BUS_ARN=$(cat /app/build/bus_arn)
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": "ReadOutboxStreamOnly",
"Effect": "Allow",
"Action": [
"dynamodb:DescribeStream",
"dynamodb:GetRecords",
"dynamodb:GetShardIterator",
"dynamodb:ListStreams"
],
"Resource": "$OUTBOX_STREAM_ARN"
},
{
"Sid": "DedupTableWrite",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:DeleteItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/processed_events"
},
{
"Sid": "PublishToOrderEventsBus",
"Effect": "Allow",
"Action": "events:PutEvents",
"Resource": "$BUS_ARN"
}
]
}
EOF
cat /app/policies/outbox-relay-policy.json | jq .{
"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": "ReadOutboxStreamOnly",
"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:57:25.643"
},
{
"Sid": "DedupTableWrite",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:DeleteItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/processed_events"
},
{
"Sid": "PublishToOrderEventsBus",
"Effect": "Allow",
"Action": "events:PutEvents",
"Resource": "arn:aws:events:us-east-1:000000000000:event-bus/order-events"
}
]
}
[stdout]
{
"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": "ReadOutboxStreamOnly",
"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:57:25.643"
},
{
"Sid": "DedupTableWrite",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:DeleteItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/processed_events"
},
{
"Sid": "PublishToOrderEventsBus",
"Effect": "Allow",
"Action": "events:PutEvents",
"Resource": "arn:aws:events:us-east-1:000000000000:event-bus/order-events"
}
]
}Create roles + attach policies
# Roles aws iam create-role --role-name order-api-role \ --assume-role-policy-document file:///app/policies/lambda-trust.json \ --query 'Role.Arn' --output text aws iam create-role --role-name outbox-relay-role \ --assume-role-policy-document file:///app/policies/lambda-trust.json \ --query 'Role.Arn' --output text # Inline policies aws iam put-role-policy --role-name order-api-role \ --policy-name order-api-inline \ --policy-document file:///app/policies/order-api-policy.json aws iam put-role-policy --role-name outbox-relay-role \ --policy-name outbox-relay-inline \ --policy-document file:///app/policies/outbox-relay-policy.json echo "policies attached:" aws iam list-role-policies --role-name order-api-role --query 'PolicyNames' --output text aws iam list-role-policies --role-name outbox-relay-role --query 'PolicyNames' --output text
arn:aws:iam::000000000000:role/order-api-role arn:aws:iam::000000000000:role/outbox-relay-role policies attached: order-api-inline outbox-relay-inline [stdout] arn:aws:iam::000000000000:role/order-api-role arn:aws:iam::000000000000:role/outbox-relay-role policies attached: order-api-inline outbox-relay-inline
Zip lambda code
cd /app/lambdas && zip -j /app/build/order-api.zip order_api.py && zip -j /app/build/outbox-relay.zip outbox_relay.py ls -la /app/build/*.zip
adding: order_api.py (deflated 59%) adding: outbox_relay.py (deflated 62%) -rw-r--r-- 1 root root 924 Apr 28 14:59 /app/build/order-api.zip -rw-r--r-- 1 root root 1175 Apr 28 14:59 /app/build/outbox-relay.zip [stdout] adding: order_api.py (deflated 59%) adding: outbox_relay.py (deflated 62%) -rw-r--r-- 1 root root 924 Apr 28 14:59 /app/build/order-api.zip -rw-r--r-- 1 root root 1175 Apr 28 14:59 /app/build/outbox-relay.zip
Create lambdas, wait for active
API_ROLE_ARN=$(aws iam get-role --role-name order-api-role --query 'Role.Arn' --output text)
RELAY_ROLE_ARN=$(aws iam get-role --role-name outbox-relay-role --query 'Role.Arn' --output text)
aws lambda create-function \
--function-name order-api \
--runtime python3.11 \
--handler order_api.handler \
--role "$API_ROLE_ARN" \
--zip-file fileb:///app/build/order-api.zip \
--timeout 15 \
--environment "Variables={LOCALSTACK_ENDPOINT=http://localstack:4566,ORDERS_TABLE=orders,OUTBOX_TABLE=outbox}" \
--query 'FunctionArn' --output text
aws lambda create-function \
--function-name outbox-relay \
--runtime python3.11 \
--handler outbox_relay.handler \
--role "$RELAY_ROLE_ARN" \
--zip-file fileb:///app/build/outbox-relay.zip \
--timeout 30 \
--environment "Variables={LOCALSTACK_ENDPOINT=http://localstack:4566,BUS_NAME=order-events,DEDUP_TABLE=processed_events,DEDUP_TTL_SECONDS=604800}" \
--query 'FunctionArn' --output text
# wait until both Active
for fn in order-api outbox-relay; do
for i in 1 2 3 4 5 6 7 8 9 10; do
STATE=$(aws lambda get-function --function-name $fn --query 'Configuration.State' --output text)
[ "$STATE" = "Active" ] && break
echo "$fn state=$STATE; waiting..."
sleep 1
done
echo "$fn final state: $(aws lambda get-function --function-name $fn --query 'Configuration.State' --output text)"
donearn:aws:lambda:us-east-1:000000000000:function:order-api arn:aws:lambda:us-east-1:000000000000:function:outbox-relay order-api state=Pending; waiting... order-api state=Pending; waiting... order-api final state: Active outbox-relay final state: Active [stdout] arn:aws:lambda:us-east-1:000000000000:function:order-api arn:aws:lambda:us-east-1:000000000000:function:outbox-relay order-api state=Pending; waiting... order-api state=Pending; waiting... order-api final state: Active outbox-relay final state: Active
Create event source mapping
OUTBOX_STREAM_ARN=$(cat /app/build/outbox_stream_arn)
aws lambda create-event-source-mapping \
--function-name outbox-relay \
--event-source-arn "$OUTBOX_STREAM_ARN" \
--starting-position LATEST \
--batch-size 10 \
--maximum-batching-window-in-seconds 1 \
--function-response-types ReportBatchItemFailures \
--query '{UUID:UUID,State:State,FunctionResponseTypes:FunctionResponseTypes}' --output json
# wait for ESM to enable
for i in 1 2 3 4 5 6 7 8 9 10; do
STATE=$(aws lambda list-event-source-mappings --function-name outbox-relay --query 'EventSourceMappings[0].State' --output text)
echo "ESM state=$STATE"
[ "$STATE" = "Enabled" ] && break
sleep 1
done{
"UUID": "d42eb0b6-f5a4-4fea-a171-daca91a01889",
"State": "Creating",
"FunctionResponseTypes": [
"ReportBatchItemFailures"
]
}
ESM state=Enabled
[stdout]
{
"UUID": "d42eb0b6-f5a4-4fea-a171-daca91a01889",
"State": "Creating",
"FunctionResponseTypes": [
"ReportBatchItemFailures"
]
}
ESM state=EnabledSmoke test: OrderCreated
CREATED_URL=$(cat /app/build/created_url)
CANCELLED_URL=$(cat /app/build/cancelled_url)
# Drain queues just in case
aws sqs purge-queue --queue-url "$CREATED_URL" 2>/dev/null
aws sqs purge-queue --queue-url "$CANCELLED_URL" 2>/dev/null
# Invoke 1
aws lambda invoke --function-name order-api \
--cli-binary-format raw-in-base64-out \
--payload '{"order_id":"o-1","kind":"OrderCreated","total":42.50,"customer":"alice"}' \
/tmp/r1.json >/dev/null
echo "invoke 1 result:" && cat /tmp/r1.json && echo
EVENT_ID_1=$(jq -r '.event_id' /tmp/r1.json)
echo "EVENT_ID_1=$EVENT_ID_1"
# Wait up to 30s for the relay to publish
for i in $(seq 1 30); do
N_CREATED=$(aws sqs get-queue-attributes --queue-url "$CREATED_URL" --attribute-names ApproximateNumberOfMessages --query 'Attributes.ApproximateNumberOfMessages' --output text)
N_CANCELLED=$(aws sqs get-queue-attributes --queue-url "$CANCELLED_URL" --attribute-names ApproximateNumberOfMessages --query 'Attributes.ApproximateNumberOfMessages' --output text)
echo "t=${i}s created=$N_CREATED cancelled=$N_CANCELLED"
if [ "$N_CREATED" = "1" ]; then break; fi
sleep 1
doneinvoke 1 result:
{"statusCode": 200, "order_id": "o-1", "event_id": "934063af-8c42-42e8-98d4-dae523ca0f3a", "kind": "OrderCreated"}
EVENT_ID_1=934063af-8c42-42e8-98d4-dae523ca0f3a
t=1s created=0 cancelled=0
t=2s created=1 cancelled=0
[stdout]
invoke 1 result:
{"statusCode": 200, "order_id": "o-1", "event_id": "934063af-8c42-42e8-98d4-dae523ca0f3a", "kind": "OrderCreated"}
EVENT_ID_1=934063af-8c42-42e8-98d4-dae523ca0f3a
t=1s created=0 cancelled=0
t=2s created=1 cancelled=0Verify created queue + dedup row
CREATED_URL=$(cat /app/build/created_url)
CANCELLED_URL=$(cat /app/build/cancelled_url)
EVENT_ID_1="934063af-8c42-42e8-98d4-dae523ca0f3a"
echo "--- orders-created msg ---"
aws sqs receive-message --queue-url "$CREATED_URL" --max-number-of-messages 10 --wait-time-seconds 1 --query 'Messages[*].Body' --output text | jq .
echo "--- orders-cancelled count (expect 0) ---"
aws sqs get-queue-attributes --queue-url "$CANCELLED_URL" --attribute-names ApproximateNumberOfMessages --query 'Attributes.ApproximateNumberOfMessages' --output text
echo "--- processed_events row for $EVENT_ID_1 ---"
aws dynamodb get-item --table-name processed_events --key "{\"event_id\":{\"S\":\"$EVENT_ID_1\"}}" --query 'Item' --output json--- orders-created msg ---
{
"version": "0",
"id": "4afb2ed0-c4b7-4e62-9d74-969eeb7ae932",
"detail-type": "OrderCreated",
"source": "outbox.relay",
"account": "000000000000",
"time": "2026-04-28T15:00:24Z",
"region": "us-east-1",
"resources": [],
"detail": {
"order_id": "o-1",
"kind": "OrderCreated",
"total": 42.5,
"customer": "alice",
"event_id": "934063af-8c42-42e8-98d4-dae523ca0f3a"
}
}
--- orders-cancelled count (expect 0) ---
0
--- processed_events row for 934063af-8c42-42e8-98d4-dae523ca0f3a ---
{
"ttl": {
"N": "1777993223"
},
"event_id": {
"S": "934063af-8c42-42e8-98d4-dae523ca0f3a"
}
}
[stdout]
--- orders-created msg ---
{
"version": "0",
"id": "4afb2ed0-c4b7-4e62-9d74-969eeb7ae932",
"detail-type": "OrderCreated",
"source": "outbox.relay",
"account": "000000000000",
"time": "2026-04-28T15:00:24Z",
"region": "us-east-1",
"resources": [],
"detail": {
"order_id": "o-1",
"kind": "OrderCreated",
"total": 42.5,
"customer": "alice",
"event_id": "934063af-8c42-42e8-98d4-dae523ca0f3a"
}
}
--- orders-cancelled count (expect 0) ---
0
--- processed_events row for 934063af-8c42-42e8-98d4-dae523ca0f3a ---
{
"ttl": {
"N": "1777993223"
},
"event_id": {
"S": "934063af-8c42-42e8-98d4-dae523ca0f3a"
}
}Smoke test: OrderCancelled
CREATED_URL=$(cat /app/build/created_url)
CANCELLED_URL=$(cat /app/build/cancelled_url)
# drain (we already received above; let it settle)
aws sqs purge-queue --queue-url "$CREATED_URL" 2>/dev/null
aws sqs purge-queue --queue-url "$CANCELLED_URL" 2>/dev/null
sleep 2
aws lambda invoke --function-name order-api \
--cli-binary-format raw-in-base64-out \
--payload '{"order_id":"o-2","kind":"OrderCancelled","reason":"customer_request"}' \
/tmp/r2.json >/dev/null
cat /tmp/r2.json && echo
EVENT_ID_2=$(jq -r '.event_id' /tmp/r2.json)
echo "EVENT_ID_2=$EVENT_ID_2"
for i in $(seq 1 30); do
N_CREATED=$(aws sqs get-queue-attributes --queue-url "$CREATED_URL" --attribute-names ApproximateNumberOfMessages --query 'Attributes.ApproximateNumberOfMessages' --output text)
N_CANCELLED=$(aws sqs get-queue-attributes --queue-url "$CANCELLED_URL" --attribute-names ApproximateNumberOfMessages --query 'Attributes.ApproximateNumberOfMessages' --output text)
echo "t=${i}s created=$N_CREATED cancelled=$N_CANCELLED"
if [ "$N_CANCELLED" = "1" ]; then break; fi
sleep 1
done
echo "--- orders-cancelled msg ---"
aws sqs receive-message --queue-url "$CANCELLED_URL" --max-number-of-messages 10 --wait-time-seconds 1 --query 'Messages[*].Body' --output text | jq .
echo "--- dedup row for cancelled ---"
aws dynamodb get-item --table-name processed_events --key "{\"event_id\":{\"S\":\"$EVENT_ID_2\"}}" --query 'Item' --output json{"statusCode": 200, "order_id": "o-2", "event_id": "754585b8-7dfc-4ec8-a1ce-9e658b3c79c6", "kind": "OrderCancelled"}
EVENT_ID_2=754585b8-7dfc-4ec8-a1ce-9e658b3c79c6
t=1s created=0 cancelled=0
t=2s created=0 cancelled=1
--- orders-cancelled msg ---
{
"version": "0",
"id": "8816074f-d77b-41c4-8b7d-1aaf051a9cbd",
"detail-type": "OrderCancelled",
"source": "outbox.relay",
"account": "000000000000",
"time": "2026-04-28T15:00:48Z",
"region": "us-east-1",
"resources": [],
"detail": {
"order_id": "o-2",
"kind": "OrderCancelled",
"reason": "customer_request",
"event_id": "754585b8-7dfc-4ec8-a1ce-9e658b3c79c6"
}
}
--- dedup row for cancelled ---
{
"ttl": {
"N": "1777993247"
},
"event_id": {
"S": "754585b8-7dfc-4ec8-a1ce-9e658b3c79c6"
}
}
[stdout]
{"statusCode": 200, "order_id": "o-2", "event_id": "754585b8-7dfc-4ec8-a1ce-9e658b3c79c6", "kind": "OrderCancelled"}
EVENT_ID_2=754585b8-7dfc-4ec8-a1ce-9e658b3c79c6
t=1s created=0 cancelled=0
t=2s created=0 cancelled=1
--- orders-cancelled msg ---
{
"version": "0",
"id": "8816074f-d77b-41c4-8b7d-1aaf051a9cbd",
"detail-type": "OrderCancelled",
"source": "outbox.relay",
"account": "000000000000",
"time": "2026-04-28T15:00:48Z",
"region": "us-east-1",
"resources": [],
"detail": {
"order_id": "o-2",
"kind": "OrderCancelled",
"reason": "customer_request",
"event_id": "754585b8-7dfc-4ec8-a1ce-9e658b3c79c6"
}
}
--- dedup row for cancelled ---
{
"ttl": {
"N": "1777993247"
},
"event_id": {
"S": "754585b8-7dfc-4ec8-a1ce-9e658b3c79c6"
}
}Final verification dump
echo "--- orders rows (atomic write proof) ---"
aws dynamodb scan --table-name orders --query 'Items' --output json
echo
echo "--- outbox rows ---"
aws dynamodb scan --table-name outbox --query 'Items' --output json
echo
echo "--- order-api role policy ---"
aws iam get-role-policy --role-name order-api-role --policy-name order-api-inline --query 'PolicyDocument' --output json
echo
echo "--- outbox-relay role policy ---"
aws iam get-role-policy --role-name outbox-relay-role --policy-name outbox-relay-inline --query 'PolicyDocument' --output json
echo
echo "--- ESM ReportBatchItemFailures ---"
aws lambda list-event-source-mappings --function-name outbox-relay --query 'EventSourceMappings[0].{State:State,FunctionResponseTypes:FunctionResponseTypes,EventSourceArn:EventSourceArn}' --output json
echo
echo "--- outbox stream view type ---"
aws dynamodb describe-table --table-name outbox --query 'Table.StreamSpecification' --output json
echo
echo "--- processed_events TTL ---"
aws dynamodb describe-time-to-live --table-name processed_events --query 'TimeToLiveDescription' --output json
echo
echo "--- KMS alias resolves ---"
aws kms describe-key --key-id alias/outbox-cmk --query '{Arn:KeyMetadata.Arn,KeyManager:KeyMetadata.KeyManager}' --output json--- orders rows (atomic write proof) ---
[
{
"total": {
"N": "42.5"
},
"updated_at": {
"S": "2026-04-28T15:00:21.743895+00:00"
},
"order_id": {
"S": "o-1"
},
"kind": {
"S": "OrderCreated"
},
"customer": {
"S": "alice"
}
},
{
"reason": {
"S": "customer_request"
},
"updated_at": {
"S": "2026-04-28T15:00:45.776803+00:00"
},
"order_id": {
"S": "o-2"
},
"kind": {
"S": "OrderCancelled"
}
}
]
--- outbox rows ---
[
{
"created_at": {
"S": "2026-04-28T15:00:45.776803+00:00"
},
"event_id": {
"S": "754585b8-7dfc-4ec8-a1ce-9e658b3c79c6"
},
"order_id": {
"S": "o-2"
},
"payload": {
"S": "{\"order_id\": \"o-2\", \"kind\": \"OrderCancelled\", \"reason\": \"customer_request\"}"
},
"kind": {
"S": "OrderCancelled"
}
},
{
"created_at": {
"S": "2026-04-28T15:00:21.743895+00:00"
},
"event_id": {
"S": "934063af-8c42-42e8-98d4-dae523ca0f3a"
},
"order_id": {
"S": "o-1"
},
"payload": {
"S": "{\"order_id\": \"o-1\", \"kind\": \"OrderCreated\", \"total\": 42.5, \"customer\": \"alice\"}"
},
"kind": {
"S": "OrderCreated"
}
}
]
--- order-api role 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": "OrdersAndOutboxTransactWrite",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DescribeTable"
],
"Resource": [
"arn:aws:dynamodb:us-east-1:000000000000:table/orders",
"arn:aws:dynamodb:us-east-1:000000000000:table/outbox"
]
},
{
"Sid": "TransactWrite",
"Effect": "Allow",
"Action": "dynamodb:TransactWriteItems",
"Resource": [
"arn:aws:dynamodb:us-east-1:000000000000:table/orders",
"arn:aws:dynamodb:us-east-1:000000000000:table/outbox"
]
}
]
}
--- outbox-relay role 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": "ReadOutboxStreamOnly",
"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:57:25.643"
},
{
"Sid": "DedupTableWrite",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:DeleteItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/processed_events"
},
{
"Sid": "PublishToOrderEventsBus",
"Effect": "Allow",
"Action": "events:PutEvents",
"Resource": "arn:aws:events:us-east-1:000000000000:event-bus/order-events"
}
]
}
--- ESM ReportBatchItemFailures ---
{
"State": "Enabled",
"FunctionResponseTypes": [
"ReportBatchItemFailures"
],
"EventSourceArn": "arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T14:57:25.643"
}
--- outbox stream view type ---
{
"StreamEnabled": true,
"StreamViewType": "NEW_AND_OLD_IMAGES"
}
--- processed_events TTL ---
{
"TimeToLiveStatus": "ENABLED",
"AttributeName": "ttl"
}
--- KMS alias resolves ---
{
"Arn": "arn:aws:kms:us-east-1:000000000000:key/d1e5c8d8-871d-4ae3-bfc5-890beb11dae3",
"KeyManager": "CUSTOMER"
}
[stdout]
--- orders rows (atomic write proof) ---
[
{
"total": {
"N": "42.5"
},
"updated_at": {
"S": "2026-04-28T15:00:21.743895+00:00"
},
"order_id": {
"S": "o-1"
},
"kind": {
"S": "OrderCreated"
},
"customer": {
"S": "alice"
}
},
{
"reason": {
"S": "customer_request"
},
"updated_at": {
"S": "2026-04-28T15:00:45.776803+00:00"
},
"order_id": {
"S": "o-2"
},
"kind": {
"S": "OrderCancelled"
}
}
]
--- outbox rows ---
[
{
"created_at": {
"S": "2026-04-28T15:00:45.776803+00:00"
},
"event_id": {
"S": "754585b8-7dfc-4ec8-a1ce-9e658b3c79c6"
},
"order_id": {
"S": "o-2"
},
"payload": {
"S": "{\"order_id\": \"o-2\", \"kind\": \"OrderCancelled\", \"reason\": \"customer_request\"}"
},
"kind": {
"S": "OrderCancelled"
}
},
{
"created_at": {
"S": "2026-04-28T15:00:21.743895+00:00"
},
"event_id": {
"S": "934063af-8c42-42e8-98d4-dae523ca0f3a"
},
"order_id": {
"S": "o-1"
},
"payload": {
"S": "{\"order_id\": \"o-1\", \"kind\": \"OrderCreated\", \"total\": 42.5, \"customer\": \"alice\"}"
},
"kind": {
"S": "OrderCreated"
}
}
]
--- order-api role 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": "OrdersAndOutboxTransactWrite",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DescribeTable"
],
"Resource": [
"arn:aws:dynamodb:us-east-1:000000000000:table/orders",
"arn:aws:dynamodb:us-east-1:000000000000:table/outbox"
]
},
{
"Sid": "TransactWrite",
"Effect": "Allow",
"Action": "dynamodb:TransactWriteItems",
"Resource": [
"arn:aws:dynamodb:us-east-1:000000000000:table/orders",
"arn:aws:dynamodb:us-east-1:000000000000:table/outbox"
]
}
]
}
--- outbox-relay role 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": "ReadOutboxStreamOnly",
"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:57:25.643"
},
{
"Sid": "DedupTableWrite",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:DeleteItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/processed_events"
},
{
"Sid": "PublishToOrderEventsBus",
"Effect": "Allow",
"Action": "events:PutEvents",
"Resource": "arn:aws:events:us-east-1:000000000000:event-bus/order-events"
}
]
}
--- ESM ReportBatchItemFailures ---
{
"State": "Enabled",
"FunctionResponseTypes": [
"ReportBatchItemFailures"
],
"EventSourceArn": "arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T14:57:25.643"
}
--- outbox stream view type ---
{
"StreamEnabled": true,
"StreamViewType": "NEW_AND_OLD_IMAGES"
}
--- processed_events TTL ---
{
"TimeToLiveStatus": "ENABLED",
"AttributeName": "ttl"
}
--- KMS alias resolves ---
{
"Arn": "arn:aws:kms:us-east-1:000000000000:key/d1e5c8d8-871d-4ae3-bfc5-890beb11dae3",
"KeyManager": "CUSTOMER"
}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/A8zCpMMJJkqPh2g2RPN5f/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 FAILED [ 30%]
../tests/test_outputs.py::test_07_outbox_table_encrypted_with_cmk FAILED [ 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 FAILED [ 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%]
=================================== FAILURES ===================================
___________________ test_06_orders_table_encrypted_with_cmk ____________________
def test_06_orders_table_encrypted_with_cmk():
ddb = _client("dynamodb")
cmk_arn, cmk_id = _cmk_arn()
t = ddb.describe_table(TableName=ORDERS_TABLE)["Table"]
sse = t.get("SSEDescription") or {}
assert sse.get("Status") == "ENABLED", "SSE must be enabled"
assert sse.get("SSEType") == "KMS", "SSE must be KMS, not default"
used = sse.get("KMSMasterKeyArn", "")
> assert cmk_id in used or cmk_arn == used, (
f"orders table must use CMK {cmk_id}, got {used}"
)
E AssertionError: orders table must use CMK d1e5c8d8-871d-4ae3-bfc5-890beb11dae3, got arn:aws:kms:us-east-1:000000000000:key/alias/outbox-cmk
E assert ('d1e5c8d8-871d-4ae3-bfc5-890beb11dae3' in 'arn:aws:kms:us-east-1:000000000000:key/alias/outbox-cmk' or 'arn:aws:kms:...-890beb11dae3' == 'arn:aws:kms:...as/outbox-cmk'
E
E - arn:aws:kms:us-east-1:000000000000:key/alias/outbox-cmk
E + arn:aws:kms:us-east-1:000000000000:key/d1e5c8d8-871d-4ae3-bfc5-890beb11dae3)
/tests/test_outputs.py:228: AssertionError
___________________ test_07_outbox_table_encrypted_with_cmk ____________________
def test_07_outbox_table_encrypted_with_cmk():
ddb = _client("dynamodb")
cmk_arn, cmk_id = _cmk_arn()
t = ddb.describe_table(TableName=OUTBOX_TABLE)["Table"]
sse = t.get("SSEDescription") or {}
assert sse.get("Status") == "ENABLED"
assert sse.get("SSEType") == "KMS"
used = sse.get("KMSMasterKeyArn", "")
> assert cmk_id in used or cmk_arn == used
E AssertionError: assert ('d1e5c8d8-871d-4ae3-bfc5-890beb11dae3' in 'arn:aws:kms:us-east-1:000000000000:key/alias/outbox-cmk' or 'arn:aws:kms:...-890beb11dae3' == 'arn:aws:kms:...as/outbox-cmk'
E
E - arn:aws:kms:us-east-1:000000000000:key/alias/outbox-cmk
E + arn:aws:kms:us-east-1:000000000000:key/d1e5c8d8-871d-4ae3-bfc5-890beb11dae3)
/tests/test_outputs.py:241: AssertionError
__________________ test_16_relay_role_is_scoped_not_wildcard ___________________
def test_16_relay_role_is_scoped_not_wildcard():
"""outbox-relay role must NOT have '*' on Resource for dynamodb stream
actions or for kms:Decrypt. These actions must be scoped to the outbox
stream ARN and to the CMK respectively."""
ddb = _client("dynamodb")
stream_arn = ddb.describe_table(TableName=OUTBOX_TABLE)["Table"]["LatestStreamArn"]
cmk_arn, cmk_id = _cmk_arn()
docs = _get_role_policies(RELAY_ROLE)
stmts = _statements(docs)
for s in stmts:
if s.get("Effect") != "Allow":
continue
actions = _as_list(s.get("Action"))
resources = _as_list(s.get("Resource"))
if any(a.startswith("dynamodb:") or a == "*" for a in actions):
for r in resources:
if r == "*":
if any(a in ("dynamodb:GetRecords", "dynamodb:GetShardIterator",
"dynamodb:DescribeStream", "dynamodb:ListStreams",
"*") for a in actions):
pytest.fail(
f"relay role wildcards dynamodb stream actions on '*': "
f"actions={actions}"
)
def stream_resource_ok(r):
return isinstance(r, str) and OUTBOX_TABLE in r and ("stream" in r.lower() or "/stream/" in r)
assert _allows(stmts, "dynamodb:GetRecords", stream_resource_ok) or _allows(
stmts, "dynamodb:GetShardIterator", stream_resource_ok
), "relay role must allow dynamodb stream actions on the outbox stream ARN"
for s in stmts:
if s.get("Effect") != "Allow":
continue
actions = _as_list(s.get("Action"))
if "kms:Decrypt" in actions or "kms:*" in actions or "*" in actions:
resources = _as_list(s.get("Resource"))
for r in resources:
if r == "*":
pytest.fail(
f"relay role wildcards kms:Decrypt on Resource '*' - "
f"must scope to CMK {cmk_id}"
)
def cmk_resource_ok(r):
return isinstance(r, str) and (cmk_id in r or KEY_ALIAS in r or r == cmk_arn)
> assert _allows(stmts, "kms:Decrypt", cmk_resource_ok), (
f"relay role must allow kms:Decrypt on the CMK ({cmk_id})"
)
E AssertionError: relay role must allow kms:Decrypt on the CMK (d1e5c8d8-871d-4ae3-bfc5-890beb11dae3)
E assert False
E + where False = _allows([{'Action': ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], 'Effect': 'Allow', 'Resource': 'arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/outbox-relay*', 'Sid': 'Logs'}, {'Action': ['dynamodb:DescribeStream', 'dynamodb:GetRecords', 'dynamodb:GetShardIterator', 'dynamodb:ListStreams'], 'Effect': 'Allow', 'Resource': 'arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T14:57:25.643', 'Sid': 'ReadOutboxStreamOnly'}, {'Action': ['dynamodb:PutItem', 'dynamodb:GetItem', 'dynamodb:DeleteItem'], 'Effect': 'Allow', 'Resource': 'arn:aws:dynamodb:us-east-1:000000000000:table/processed_events', 'Sid': 'DedupTableWrite'}, {'Action': 'events:PutEvents', 'Effect': 'Allow', 'Resource': 'arn:aws:events:us-east-1:000000000000:event-bus/order-events', 'Sid': 'PublishToOrderEventsBus'}], 'kms:Decrypt', <function test_16_relay_role_is_scoped_not_wildcard.<locals>.cmk_resource_ok at 0xffff7d5336a0>)
/tests/test_outputs.py:482: AssertionError
=============================== warnings summary ===============================
test_outputs.py: 91 warnings
/root/.cache/uv/archive-v0/A8zCpMMJJkqPh2g2RPN5f/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_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_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
FAILED ../tests/test_outputs.py::test_06_orders_table_encrypted_with_cmk - As...
FAILED ../tests/test_outputs.py::test_07_outbox_table_encrypted_with_cmk - As...
FAILED ../tests/test_outputs.py::test_16_relay_role_is_scoped_not_wildcard - ...
================== 3 failed, 17 passed, 91 warnings in 22.42s ==================Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_964eb40653844d05. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_964eb40653844d05 · verifier authoritative; classifier explanatory.