Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d50e429
Install Terraform in devcontainer via HashiCorp apt repo
whummer Apr 18, 2026
46eb93a
Add LOCALSTACK_DEBUG=1 to devcontainer env
whummer Apr 18, 2026
9519dcf
Remove AWS_ENDPOINT_URL override from Lambda env — LocalStack auto-pa…
whummer Apr 18, 2026
4c6eb12
Skip tflocal init in deploy if .terraform dir already exists
whummer Apr 18, 2026
7704d47
Fix missing os import in Lambda handlers
whummer Apr 18, 2026
43e39fe
Replace auto-refresh with manual refresh button; reload after order p…
whummer Apr 18, 2026
4e91269
Fix Decimal serialization error in list_orders
whummer Apr 18, 2026
66ddd6f
Add Step Functions state machine for async multi-step order processing
whummer Apr 18, 2026
68cb32a
Fix Terraform cycle: derive state machine ARN from caller identity
whummer Apr 18, 2026
21fc142
Add processing delays, step timestamps, and expandable order rows wit…
whummer Apr 18, 2026
539b76d
Increase order_processor Lambda timeout to 30s
whummer Apr 18, 2026
b32820f
Fix chaos fault JSON: remove unsupported 'rate' field
whummer Apr 18, 2026
758f939
Fix remove-fault: send empty JSON body for DELETE chaos faults
whummer Apr 18, 2026
ef12588
Fix empty DLQ replay; add chaos status banner to UI
whummer Apr 18, 2026
0006ddb
Fix SFN execution name collision on DLQ replay
whummer Apr 18, 2026
ba866e6
Set status before starting SFN so DDB faults surface to SQS/DLQ
whummer Apr 18, 2026
9f1207a
Fix remove-fault: use POST [] to clear faults instead of DELETE
whummer Apr 18, 2026
f794a69
Add DLQ auto-resume checkbox and richer order step details in UI
whummer Apr 18, 2026
900d89b
Add collapsible info panel with architecture and chaos engineering notes
whummer Apr 18, 2026
751cc16
Restructure UI: three-column layout with left nav and right sidebar
whummer Apr 18, 2026
8af44c4
Force API GW redeployment via triggers hash
whummer Apr 18, 2026
126f5e7
Shorten order ID to 12 hex chars; show full ID in table
whummer Apr 18, 2026
82bbb75
Remove auto-resume DLQ checkbox — DLQ replay happens automatically
whummer Apr 18, 2026
9474525
Add Mermaid architecture diagram; remove auto-DLQ references from About
whummer Apr 18, 2026
8be17cc
Abbreviate fault error code in sidebar using initials
whummer Apr 18, 2026
0caec3d
Fix Mermaid rendering: defer until About section is visible
whummer Apr 18, 2026
eae7d46
Add Products: DynamoDB table, pre-seeded swag, API endpoint, UI page …
whummer Apr 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .devcontainer/Dockerfile.github
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ ENV EXTRA_CORS_ALLOWED_ORIGINS='*'
ENV DISABLE_CUSTOM_CORS_APIGATEWAY=1
ENV LOCALSTACK_APPINSPECTOR_ENABLE=1
ENV LOCALSTACK_APPINSPECTOR_DEV_ENABLE=1
ENV LOCALSTACK_DEBUG=1

# Workshop token URL — set by organizer before each event.
# The setup script fetches the actual token from this endpoint.
Expand Down
52 changes: 46 additions & 6 deletions 01-serverless-app/lambdas/order_handler/handler.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import json
import os
import uuid
from datetime import datetime, timezone
from decimal import Decimal
import boto3

dynamodb = boto3.resource("dynamodb", endpoint_url=os.environ.get("AWS_ENDPOINT_URL"))
sqs = boto3.client("sqs", endpoint_url=os.environ.get("AWS_ENDPOINT_URL"))
dynamodb = boto3.resource("dynamodb")
sqs = boto3.client("sqs")

TABLE_NAME = os.environ["ORDERS_TABLE"]
PRODUCTS_TABLE = os.environ["PRODUCTS_TABLE"]
QUEUE_URL = os.environ["ORDERS_QUEUE_URL"]
DLQ_URL = os.environ.get("ORDERS_DLQ_URL", "")

class DecimalEncoder(json.JSONEncoder):
def default(self, o):
return int(o) if isinstance(o, Decimal) else super().default(o)

TABLE_NAME = os.environ["ORDERS_TABLE"]
QUEUE_URL = os.environ["ORDERS_QUEUE_URL"]

CORS_HEADERS = {
"Access-Control-Allow-Origin": "*",
Expand All @@ -18,10 +27,17 @@

def handler(event, context):
method = event.get("httpMethod", "")
path = event.get("path", "")

if method == "OPTIONS":
return {"statusCode": 200, "headers": CORS_HEADERS, "body": ""}

if method == "POST" and path.endswith("/replay"):
return replay_dlq()

if method == "GET" and "/products" in path:
return list_products()

if method == "GET":
return list_orders()

Expand All @@ -31,26 +47,50 @@ def handler(event, context):
return {"statusCode": 405, "headers": CORS_HEADERS, "body": "Method Not Allowed"}


def replay_dlq():
resp = sqs.receive_message(QueueUrl=DLQ_URL, MaxNumberOfMessages=10)
messages = resp.get("Messages", [])
for msg in messages:
sqs.send_message(QueueUrl=QUEUE_URL, MessageBody=msg["Body"])
sqs.delete_message(QueueUrl=DLQ_URL, ReceiptHandle=msg["ReceiptHandle"])
return {
"statusCode": 200,
"headers": {**CORS_HEADERS, "Content-Type": "application/json"},
"body": json.dumps({"replayed": len(messages)}),
}


def list_products():
table = dynamodb.Table(PRODUCTS_TABLE)
items = sorted(table.scan().get("Items", []), key=lambda x: x.get("name", ""))
return {
"statusCode": 200,
"headers": {**CORS_HEADERS, "Content-Type": "application/json"},
"body": json.dumps(items, cls=DecimalEncoder),
}


def list_orders():
table = dynamodb.Table(TABLE_NAME)
result = table.scan()
items = sorted(result.get("Items", []), key=lambda x: x.get("order_id", ""))
return {
"statusCode": 200,
"headers": {**CORS_HEADERS, "Content-Type": "application/json"},
"body": json.dumps(items),
"body": json.dumps(items, cls=DecimalEncoder),
}


def create_order(event):
body = json.loads(event.get("body") or "{}")
order_id = str(uuid.uuid4())
order_id = uuid.uuid4().hex[:12]

order = {
"order_id": order_id,
"item": body.get("item", "unknown"),
"quantity": int(body.get("quantity", 1)),
"status": "pending",
"created_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
}

table = dynamodb.Table(TABLE_NAME)
Expand Down
119 changes: 91 additions & 28 deletions 01-serverless-app/lambdas/order_processor/handler.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,101 @@
import json
import os
import time
import uuid
from datetime import datetime, timezone
import boto3

dynamodb = boto3.resource("dynamodb", endpoint_url=os.environ.get("AWS_ENDPOINT_URL"))
s3 = boto3.client("s3", endpoint_url=os.environ.get("AWS_ENDPOINT_URL"))
dynamodb = boto3.resource("dynamodb")
s3 = boto3.client("s3")
sfn = boto3.client("stepfunctions")

TABLE_NAME = os.environ["ORDERS_TABLE"]
RECEIPTS_BUCKET = os.environ["RECEIPTS_BUCKET"]
STATE_MACHINE_ARN = os.environ["STATE_MACHINE_ARN"]

TERMINAL_STATUSES = {"fulfilled", "failed"}


def now():
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def set_status(order_id, status):
ts_key = {
"validating": "validating_at",
"payment_processing": "payment_at",
"fulfilled": "fulfilled_at",
"failed": "failed_at",
}.get(status)

expression = "SET #s = :s"
names = {"#s": "status"}
values = {":s": status}

if ts_key:
expression += ", #ts = :ts"
names["#ts"] = ts_key
values[":ts"] = now()

dynamodb.Table(TABLE_NAME).update_item(
Key={"order_id": order_id},
UpdateExpression=expression,
ExpressionAttributeNames=names,
ExpressionAttributeValues=values,
)


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

# Update order status in DynamoDB
table = dynamodb.Table(TABLE_NAME)
table.update_item(
Key={"order_id": order_id},
UpdateExpression="SET #s = :s",
ExpressionAttributeNames={"#s": "status"},
ExpressionAttributeValues={":s": "processed"},
)

# Store receipt in S3
receipt = {
"order_id": order_id,
"item": order.get("item"),
"quantity": order.get("quantity"),
"status": "processed",
}
s3.put_object(
Bucket=RECEIPTS_BUCKET,
Key=f"receipts/{order_id}.json",
Body=json.dumps(receipt),
ContentType="application/json",
)
# Triggered by SQS: start a state machine execution per order
if "Records" in event:
for record in event["Records"]:
order = json.loads(record["body"])
set_status(order["order_id"], "validating") # fails fast if DDB is faulted → SQS retry → DLQ
sfn.start_execution(
stateMachineArn=STATE_MACHINE_ARN,
name=f"order-{order['order_id']}-{uuid.uuid4().hex[:8]}",
input=json.dumps({"order": order}),
)
return

# Invoked by Step Functions
step = event["step"]
order = event["order"]

if step == "validate": return validate(order)
if step == "process_payment": return process_payment(order)
if step == "fulfill": return fulfill(order)
if step == "handle_failure": return handle_failure(order)

raise ValueError(f"Unknown step: {step}")


def validate(order):
time.sleep(2)
set_status(order["order_id"], "validating")
return order


def process_payment(order):
time.sleep(3)
set_status(order["order_id"], "payment_processing")
return order


def fulfill(order):
time.sleep(2)
set_status(order["order_id"], "fulfilled")
receipt = {k: order[k] for k in ("order_id", "item", "quantity")}
receipt["status"] = "fulfilled"
s3.put_object(
Bucket=RECEIPTS_BUCKET,
Key=f"receipts/{order['order_id']}.json",
Body=json.dumps(receipt),
ContentType="application/json",
)
return order


def handle_failure(order):
set_status(order["order_id"], "failed")
return order
Loading
Loading