From d50e429083cc1baf71c4d78372f1ab3a1684c4fd Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Sat, 18 Apr 2026 15:41:33 +0200 Subject: [PATCH 01/27] Install Terraform in devcontainer via HashiCorp apt repo Co-Authored-By: Claude Sonnet 4.6 --- 01-serverless-app/terraform/main.tf | 9 +++++++-- .../website/{index.html.tpl => index.html} | 15 +++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) rename 01-serverless-app/website/{index.html.tpl => index.html} (91%) diff --git a/01-serverless-app/terraform/main.tf b/01-serverless-app/terraform/main.tf index 0638c27..58e30ff 100644 --- a/01-serverless-app/terraform/main.tf +++ b/01-serverless-app/terraform/main.tf @@ -159,6 +159,9 @@ resource "aws_lambda_event_source_mapping" "sqs_to_processor" { resource "aws_api_gateway_rest_api" "orders_api" { name = "orders-api" + tags = { + "_custom_id_" = "workshop" + } } resource "aws_api_gateway_resource" "orders" { @@ -236,7 +239,8 @@ resource "aws_api_gateway_deployment" "orders_api" { # ── S3 Website ──────────────────────────────────────────────────────────────── locals { - api_endpoint = "http://localhost:4566/restapis/${aws_api_gateway_rest_api.orders_api.id}/local/_user_request_" + api_id = "workshop" + api_endpoint = "http://localhost:4566/restapis/${local.api_id}/local/_user_request_" } resource "aws_s3_bucket" "website" { @@ -273,8 +277,9 @@ resource "aws_s3_bucket_policy" "website" { resource "aws_s3_object" "index_html" { bucket = aws_s3_bucket.website.id key = "index.html" - content = templatefile("${path.module}/../website/index.html.tpl", { api_endpoint = local.api_endpoint }) + source = "${path.module}/../website/index.html" content_type = "text/html" + etag = filemd5("${path.module}/../website/index.html") } output "api_endpoint" { diff --git a/01-serverless-app/website/index.html.tpl b/01-serverless-app/website/index.html similarity index 91% rename from 01-serverless-app/website/index.html.tpl rename to 01-serverless-app/website/index.html index 6bb8463..14d1699 100644 --- a/01-serverless-app/website/index.html.tpl +++ b/01-serverless-app/website/index.html @@ -106,7 +106,7 @@

Order Processing Pipeline

-

API endpoint: ${api_endpoint}

+

API endpoint:

New Order

@@ -146,11 +146,14 @@

Orders

From 4e91269c482344a2cbe06d5701b3e188268e5d67 Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Sat, 18 Apr 2026 16:18:14 +0200 Subject: [PATCH 07/27] Fix Decimal serialization error in list_orders DynamoDB returns numeric fields as Decimal; use a custom JSONEncoder to convert them to int before serializing. Co-Authored-By: Claude Sonnet 4.6 --- 01-serverless-app/lambdas/order_handler/handler.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/01-serverless-app/lambdas/order_handler/handler.py b/01-serverless-app/lambdas/order_handler/handler.py index 995f55f..10d4af4 100644 --- a/01-serverless-app/lambdas/order_handler/handler.py +++ b/01-serverless-app/lambdas/order_handler/handler.py @@ -1,6 +1,7 @@ import json import os import uuid +from decimal import Decimal import boto3 dynamodb = boto3.resource("dynamodb") @@ -9,6 +10,11 @@ TABLE_NAME = os.environ["ORDERS_TABLE"] QUEUE_URL = os.environ["ORDERS_QUEUE_URL"] +class DecimalEncoder(json.JSONEncoder): + def default(self, o): + return int(o) if isinstance(o, Decimal) else super().default(o) + + CORS_HEADERS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Content-Type", @@ -38,7 +44,7 @@ def list_orders(): return { "statusCode": 200, "headers": {**CORS_HEADERS, "Content-Type": "application/json"}, - "body": json.dumps(items), + "body": json.dumps(items, cls=DecimalEncoder), } From 66ddd6f4e65f53b8d43fe0043cbd3e4ea380115e Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Sat, 18 Apr 2026 16:26:13 +0200 Subject: [PATCH 08/27] Add Step Functions state machine for async multi-step order processing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - order_processor now starts a Step Functions execution when triggered by SQS, and handles individual steps (validate, process_payment, fulfill, handle_failure) when invoked by the state machine - State machine: ValidateOrder → wait 3s → ProcessPayment → wait 3s → FulfillOrder, with HandleFailure catch on all steps - DDB status transitions: pending → validating → payment_processing → fulfilled (or failed) - order_handler: add created_at timestamp to each order - UI: add Created column, pipeline progress indicator per order, updated status badges for all new states; sort orders newest-first - main.tf: remove redundant endpoints{} block (tflocal handles this), add SFN state machine + IAM role, STATE_MACHINE_ARN env var Co-Authored-By: Claude Sonnet 4.6 --- .../lambdas/order_handler/handler.py | 2 + .../lambdas/order_processor/handler.py | 90 ++++++++++---- 01-serverless-app/terraform/main.tf | 115 ++++++++++++++++-- 01-serverless-app/website/index.html | 103 ++++++++++++---- 4 files changed, 248 insertions(+), 62 deletions(-) diff --git a/01-serverless-app/lambdas/order_handler/handler.py b/01-serverless-app/lambdas/order_handler/handler.py index 10d4af4..df95f97 100644 --- a/01-serverless-app/lambdas/order_handler/handler.py +++ b/01-serverless-app/lambdas/order_handler/handler.py @@ -1,6 +1,7 @@ import json import os import uuid +from datetime import datetime, timezone from decimal import Decimal import boto3 @@ -57,6 +58,7 @@ def create_order(event): "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) diff --git a/01-serverless-app/lambdas/order_processor/handler.py b/01-serverless-app/lambdas/order_processor/handler.py index 8246cb6..058baa2 100644 --- a/01-serverless-app/lambdas/order_processor/handler.py +++ b/01-serverless-app/lambdas/order_processor/handler.py @@ -4,35 +4,73 @@ 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"] 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: kick off the state machine for each order + if "Records" in event: + for record in event["Records"]: + order = json.loads(record["body"]) + sfn.start_execution( + stateMachineArn=STATE_MACHINE_ARN, + name=f"order-{order['order_id']}", + input=json.dumps({"order": order}), + ) + return + + # Invoked by Step Functions with a step parameter + 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 set_status(order_id, status): + dynamodb.Table(TABLE_NAME).update_item( + Key={"order_id": order_id}, + UpdateExpression="SET #s = :s", + ExpressionAttributeNames={"#s": "status"}, + ExpressionAttributeValues={":s": status}, + ) + + +def validate(order): + set_status(order["order_id"], "validating") + return order + + +def process_payment(order): + set_status(order["order_id"], "payment_processing") + return order + + +def fulfill(order): + 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 diff --git a/01-serverless-app/terraform/main.tf b/01-serverless-app/terraform/main.tf index b509837..3bf0f28 100644 --- a/01-serverless-app/terraform/main.tf +++ b/01-serverless-app/terraform/main.tf @@ -14,15 +14,6 @@ provider "aws" { skip_credentials_validation = true skip_metadata_api_check = true skip_requesting_account_id = true - - endpoints { - apigateway = "http://localhost:4566" - dynamodb = "http://localhost:4566" - iam = "http://localhost:4566" - lambda = "http://localhost:4566" - s3 = "http://localhost:4566" - sqs = "http://localhost:4566" - } } # ── DynamoDB ────────────────────────────────────────────────────────────────── @@ -81,8 +72,8 @@ resource "aws_iam_role_policy" "lambda_policy" { Version = "2012-10-17" Statement = [ { - Effect = "Allow" - Action = ["dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:Scan"] + Effect = "Allow" + Action = ["dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:Scan"] Resource = aws_dynamodb_table.orders.arn }, { @@ -94,11 +85,42 @@ resource "aws_iam_role_policy" "lambda_policy" { Effect = "Allow" Action = ["s3:PutObject", "s3:GetObject"] Resource = "${aws_s3_bucket.receipts.arn}/*" + }, + { + Effect = "Allow" + Action = "states:StartExecution" + Resource = aws_sfn_state_machine.order_processing.arn } ] }) } +resource "aws_iam_role" "sfn_exec" { + name = "sfn-exec-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { Service = "states.amazonaws.com" } + }] + }) +} + +resource "aws_iam_role_policy" "sfn_policy" { + role = aws_iam_role.sfn_exec.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Action = "lambda:InvokeFunction" + Resource = aws_lambda_function.order_processor.arn + }] + }) +} + # ── Lambda: order_handler ───────────────────────────────────────────────────── data "archive_file" "order_handler" { @@ -141,12 +163,79 @@ resource "aws_lambda_function" "order_processor" { environment { variables = { - ORDERS_TABLE = aws_dynamodb_table.orders.name - RECEIPTS_BUCKET = aws_s3_bucket.receipts.bucket + ORDERS_TABLE = aws_dynamodb_table.orders.name + RECEIPTS_BUCKET = aws_s3_bucket.receipts.bucket + STATE_MACHINE_ARN = aws_sfn_state_machine.order_processing.arn } } } +# ── Step Functions ──────────────────────────────────────────────────────────── + +resource "aws_sfn_state_machine" "order_processing" { + name = "order-processing" + role_arn = aws_iam_role.sfn_exec.arn + + definition = jsonencode({ + StartAt = "ValidateOrder" + States = { + ValidateOrder = { + Type = "Task" + Resource = aws_lambda_function.order_processor.arn + Parameters = { + "step" = "validate" + "order.$" = "$.order" + } + ResultPath = "$.order" + Catch = [{ ErrorEquals = ["States.ALL"], Next = "HandleFailure", ResultPath = "$.error" }] + Next = "WaitForPayment" + } + WaitForPayment = { + Type = "Wait" + Seconds = 3 + Next = "ProcessPayment" + } + ProcessPayment = { + Type = "Task" + Resource = aws_lambda_function.order_processor.arn + Parameters = { + "step" = "process_payment" + "order.$" = "$.order" + } + ResultPath = "$.order" + Catch = [{ ErrorEquals = ["States.ALL"], Next = "HandleFailure", ResultPath = "$.error" }] + Next = "WaitForFulfillment" + } + WaitForFulfillment = { + Type = "Wait" + Seconds = 3 + Next = "FulfillOrder" + } + FulfillOrder = { + Type = "Task" + Resource = aws_lambda_function.order_processor.arn + Parameters = { + "step" = "fulfill" + "order.$" = "$.order" + } + ResultPath = "$.order" + Catch = [{ ErrorEquals = ["States.ALL"], Next = "HandleFailure", ResultPath = "$.error" }] + End = true + } + HandleFailure = { + Type = "Task" + Resource = aws_lambda_function.order_processor.arn + Parameters = { + "step" = "handle_failure" + "order.$" = "$.order" + } + ResultPath = "$.order" + End = true + } + } + }) +} + resource "aws_lambda_event_source_mapping" "sqs_to_processor" { event_source_arn = aws_sqs_queue.orders.arn function_name = aws_lambda_function.order_processor.arn diff --git a/01-serverless-app/website/index.html b/01-serverless-app/website/index.html index 7abb412..c89f6b0 100644 --- a/01-serverless-app/website/index.html +++ b/01-serverless-app/website/index.html @@ -21,11 +21,10 @@ align-items: center; gap: 1rem; } - header img { height: 32px; } header h1 { font-size: 1.2rem; font-weight: 600; } header span { font-size: 0.85rem; color: #aaa; margin-left: auto; } - main { max-width: 900px; margin: 2rem auto; padding: 0 1.5rem; } + main { max-width: 1000px; margin: 2rem auto; padding: 0 1.5rem; } .card { background: #fff; @@ -70,11 +69,19 @@ .flash.error { background: #fde8e8; color: #c0392b; display: block; } .table-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; } - .refresh-note { font-size: 0.78rem; color: #888; } + .refresh-btn { + padding: 0.3rem 0.9rem; + border: 1px solid #ddd; + border-radius: 6px; + background: #fff; + cursor: pointer; + font-size: 0.85rem; + } + .refresh-btn:hover { background: #f5f5f5; } table { width: 100%; border-collapse: collapse; font-size: 0.875rem; } - th { text-align: left; padding: 0.5rem 0.75rem; color: #666; font-weight: 600; border-bottom: 2px solid #eee; } - td { padding: 0.6rem 0.75rem; border-bottom: 1px solid #f0f0f0; } + th { text-align: left; padding: 0.5rem 0.75rem; color: #666; font-weight: 600; border-bottom: 2px solid #eee; white-space: nowrap; } + td { padding: 0.6rem 0.75rem; border-bottom: 1px solid #f0f0f0; vertical-align: middle; } tr:last-child td { border-bottom: none; } tr:hover td { background: #fafafa; } @@ -82,16 +89,38 @@ display: inline-block; padding: 0.2rem 0.6rem; border-radius: 99px; - font-size: 0.78rem; + font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; + white-space: nowrap; } - .badge.pending { background: #fff3cd; color: #856404; } - .badge.processed { background: #d1f5d3; color: #1a7a2a; } - .badge.failed { background: #fde8e8; color: #c0392b; } + .badge.pending { background: #fff3cd; color: #856404; } + .badge.validating { background: #cfe2ff; color: #084298; } + .badge.payment_processing { background: #e0d7ff; color: #4a1a8a; } + .badge.fulfilled { background: #d1f5d3; color: #1a7a2a; } + .badge.failed { background: #fde8e8; color: #c0392b; } - .order-id { font-family: monospace; font-size: 0.8rem; color: #888; } + .pipeline { + display: flex; + align-items: center; + gap: 0; + font-size: 0.7rem; + white-space: nowrap; + } + .pipeline-step { + padding: 0.15rem 0.5rem; + border-radius: 3px; + color: #999; + background: #f0f0f0; + } + .pipeline-step.done { background: #d1f5d3; color: #1a7a2a; } + .pipeline-step.active { background: #cfe2ff; color: #084298; font-weight: 600; } + .pipeline-step.failed { background: #fde8e8; color: #c0392b; } + .pipeline-arrow { color: #ccc; padding: 0 2px; } + + .order-id { font-family: monospace; font-size: 0.78rem; color: #888; } + .timestamp { font-size: 0.78rem; color: #999; white-space: nowrap; } .empty { text-align: center; color: #aaa; padding: 2rem; } #api-bar { font-size: 0.78rem; color: #aaa; margin-bottom: 1rem; } @@ -127,7 +156,7 @@

New Order

Orders

- +
@@ -135,41 +164,70 @@

Orders

+ + - +
Order ID Item QtyCreated StatusPipeline
Loading…
Loading…
diff --git a/04-chaos-engineering/scripts/replay_dlq.py b/04-chaos-engineering/scripts/replay_dlq.py index 9566c3e..37c59dd 100644 --- a/04-chaos-engineering/scripts/replay_dlq.py +++ b/04-chaos-engineering/scripts/replay_dlq.py @@ -15,7 +15,12 @@ aws_secret_access_key="test", ) -data = json.load(sys.stdin) +raw = sys.stdin.read().strip() +if not raw: + print("DLQ is empty.") + sys.exit(0) + +data = json.loads(raw) messages = data.get("Messages", []) if not messages: From 0006ddb4e11c164f057a436dba704c0ccde22c74 Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Sat, 18 Apr 2026 16:57:23 +0200 Subject: [PATCH 15/27] Fix SFN execution name collision on DLQ replay Appending a short random suffix allows retried orders to start a fresh execution without hitting ExecutionAlreadyExists. Co-Authored-By: Claude Sonnet 4.6 --- 01-serverless-app/lambdas/order_processor/handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/01-serverless-app/lambdas/order_processor/handler.py b/01-serverless-app/lambdas/order_processor/handler.py index cdf1a21..86b62ad 100644 --- a/01-serverless-app/lambdas/order_processor/handler.py +++ b/01-serverless-app/lambdas/order_processor/handler.py @@ -1,6 +1,7 @@ import json import os import time +import uuid from datetime import datetime, timezone import boto3 @@ -51,7 +52,7 @@ def handler(event, context): order = json.loads(record["body"]) sfn.start_execution( stateMachineArn=STATE_MACHINE_ARN, - name=f"order-{order['order_id']}", + name=f"order-{order['order_id']}-{uuid.uuid4().hex[:8]}", input=json.dumps({"order": order}), ) return From ba866e683ef98ec75bd6c8b15e8ed42e63c9f02b Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Sat, 18 Apr 2026 16:59:51 +0200 Subject: [PATCH 16/27] Set status before starting SFN so DDB faults surface to SQS/DLQ Without this, the SQS consumer Lambda always succeeded (SFN is async), so DDB throttle errors never caused SQS retries or DLQ delivery. Co-Authored-By: Claude Sonnet 4.6 --- 01-serverless-app/lambdas/order_processor/handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/01-serverless-app/lambdas/order_processor/handler.py b/01-serverless-app/lambdas/order_processor/handler.py index 86b62ad..88d60af 100644 --- a/01-serverless-app/lambdas/order_processor/handler.py +++ b/01-serverless-app/lambdas/order_processor/handler.py @@ -50,6 +50,7 @@ def handler(event, context): 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]}", From 9f1207a501b5b7b53b2a72f29b8f294d3fc9a426 Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Sat, 18 Apr 2026 17:04:06 +0200 Subject: [PATCH 17/27] Fix remove-fault: use POST [] to clear faults instead of DELETE Co-Authored-By: Claude Sonnet 4.6 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 42a9c14..cf14524 100644 --- a/Makefile +++ b/Makefile @@ -64,7 +64,7 @@ inject-fault: ## Inject DynamoDB throttling fault (breaks order_processor) -d @04-chaos-engineering/faults/ddb-throttle-localstack.json | python3 -m json.tool remove-fault: ## Remove all active fault injections - curl -s -X DELETE http://localhost:4566/_localstack/chaos/faults \ + curl -s -X POST http://localhost:4566/_localstack/chaos/faults \ -H "Content-Type: application/json" -d '[]' replay-dlq: ## Replay messages from the DLQ back to the main queue From f794a69c8dd35805c5a424816e5747e82671624e Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Sat, 18 Apr 2026 17:07:49 +0200 Subject: [PATCH 18/27] Add DLQ auto-resume checkbox and richer order step details in UI - order_handler: add POST /orders/replay endpoint to drain DLQ - terraform: add ORDERS_DLQ_URL env var and /orders/replay API GW route - index.html: checkbox to poll /replay every 5s, flashes count on replay - index.html: detail rows now show step description alongside timestamp Co-Authored-By: Claude Sonnet 4.6 --- .../lambdas/order_handler/handler.py | 18 +++++ 01-serverless-app/terraform/main.tf | 41 ++++++++++ 01-serverless-app/website/index.html | 80 ++++++++++++++++--- 3 files changed, 128 insertions(+), 11 deletions(-) diff --git a/01-serverless-app/lambdas/order_handler/handler.py b/01-serverless-app/lambdas/order_handler/handler.py index df95f97..ccdc000 100644 --- a/01-serverless-app/lambdas/order_handler/handler.py +++ b/01-serverless-app/lambdas/order_handler/handler.py @@ -10,6 +10,7 @@ TABLE_NAME = os.environ["ORDERS_TABLE"] QUEUE_URL = os.environ["ORDERS_QUEUE_URL"] +DLQ_URL = os.environ.get("ORDERS_DLQ_URL", "") class DecimalEncoder(json.JSONEncoder): def default(self, o): @@ -25,10 +26,14 @@ def default(self, o): 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": return list_orders() @@ -38,6 +43,19 @@ 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_orders(): table = dynamodb.Table(TABLE_NAME) result = table.scan() diff --git a/01-serverless-app/terraform/main.tf b/01-serverless-app/terraform/main.tf index e6dca43..68ef3b5 100644 --- a/01-serverless-app/terraform/main.tf +++ b/01-serverless-app/terraform/main.tf @@ -141,6 +141,7 @@ resource "aws_lambda_function" "order_handler" { variables = { ORDERS_TABLE = aws_dynamodb_table.orders.name ORDERS_QUEUE_URL = aws_sqs_queue.orders.url + ORDERS_DLQ_URL = aws_sqs_queue.orders_dlq.url } } } @@ -306,6 +307,44 @@ resource "aws_api_gateway_integration" "options_order_handler" { uri = aws_lambda_function.order_handler.invoke_arn } +resource "aws_api_gateway_resource" "orders_replay" { + rest_api_id = aws_api_gateway_rest_api.orders_api.id + parent_id = aws_api_gateway_resource.orders.id + path_part = "replay" +} + +resource "aws_api_gateway_method" "post_replay" { + rest_api_id = aws_api_gateway_rest_api.orders_api.id + resource_id = aws_api_gateway_resource.orders_replay.id + http_method = "POST" + authorization = "NONE" +} + +resource "aws_api_gateway_method" "options_replay" { + rest_api_id = aws_api_gateway_rest_api.orders_api.id + resource_id = aws_api_gateway_resource.orders_replay.id + http_method = "OPTIONS" + authorization = "NONE" +} + +resource "aws_api_gateway_integration" "post_replay_handler" { + rest_api_id = aws_api_gateway_rest_api.orders_api.id + resource_id = aws_api_gateway_resource.orders_replay.id + http_method = aws_api_gateway_method.post_replay.http_method + integration_http_method = "POST" + type = "AWS_PROXY" + uri = aws_lambda_function.order_handler.invoke_arn +} + +resource "aws_api_gateway_integration" "options_replay_handler" { + rest_api_id = aws_api_gateway_rest_api.orders_api.id + resource_id = aws_api_gateway_resource.orders_replay.id + http_method = aws_api_gateway_method.options_replay.http_method + integration_http_method = "POST" + type = "AWS_PROXY" + uri = aws_lambda_function.order_handler.invoke_arn +} + resource "aws_lambda_permission" "apigw" { action = "lambda:InvokeFunction" function_name = aws_lambda_function.order_handler.function_name @@ -321,6 +360,8 @@ resource "aws_api_gateway_deployment" "orders_api" { aws_api_gateway_integration.post_order_handler, aws_api_gateway_integration.get_orders_handler, aws_api_gateway_integration.options_order_handler, + aws_api_gateway_integration.post_replay_handler, + aws_api_gateway_integration.options_replay_handler, ] } diff --git a/01-serverless-app/website/index.html b/01-serverless-app/website/index.html index ebf6d38..226fb1e 100644 --- a/01-serverless-app/website/index.html +++ b/01-serverless-app/website/index.html @@ -143,6 +143,19 @@ .timestamp { font-size: 0.78rem; color: #999; white-space: nowrap; } .empty { text-align: center; color: #aaa; padding: 2rem; } + .dlq-label { + display: flex; + align-items: center; + gap: 0.35rem; + font-size: 0.82rem; + color: #555; + cursor: pointer; + user-select: none; + } + .dlq-label input { cursor: pointer; } + #dlq-indicator { font-size: 0.78rem; color: #1a7a2a; display: none; } + #dlq-indicator.active { display: inline; } + #api-bar { font-size: 0.78rem; color: #aaa; margin-bottom: 1rem; } #api-bar code { background: #f0f0f0; padding: 0.1rem 0.4rem; border-radius: 4px; } @@ -196,6 +209,10 @@

New Order

Orders

+ ↺ replayed from DLQ + ↻ auto-refreshing…
@@ -249,17 +266,32 @@

Orders

} function detailRow(o) { - const fields = [ - ["Full Order ID", `${o.order_id}`], - ["Created", `${fmt(o.created_at)}`], - ["Validating at", `${fmt(o.validating_at)}`], - ["Payment at", `${fmt(o.payment_at)}`], - ["Fulfilled at", `${fmt(o.fulfilled_at)}`], - ["Failed at", `${fmt(o.failed_at)}`], - ].filter(([, v]) => !v.includes(">—<")) - .map(([label, val]) => `
${label}${val}
`) - .join(""); - return `
${fields}
`; + const steps = [ + { label: "Pending", ts: o.created_at, desc: "Order received, queued for processing" }, + { label: "Validating", ts: o.validating_at, desc: "Checking order details" }, + { label: "Payment", ts: o.payment_at, desc: "Processing payment" }, + { label: "Fulfilled", ts: o.fulfilled_at, desc: "Receipt stored in S3" }, + { label: "Failed", ts: o.failed_at, desc: "Processing error — check DLQ" }, + ].filter(s => s.ts); + + const stepHtml = steps.map(s => ` +
+ ${s.label} + ${fmt(s.ts)} + ${s.desc} +
`).join(""); + + const meta = ` +
+ Order ID + ${o.order_id} +
+
+ Item + ${escHtml(o.item)} × ${o.quantity} +
`; + + return `
${meta}${stepHtml}
`; } function toggleDetail(id) { @@ -373,6 +405,32 @@

Orders

} catch (e) { /* LocalStack not reachable, ignore */ } } + let dlqTimer = null; + + async function replayDlq() { + try { + const res = await fetch(`${API}/replay`, { method: "POST" }); + const data = await res.json(); + if (data.replayed > 0) { + const ind = document.getElementById("dlq-indicator"); + ind.textContent = `↺ replayed ${data.replayed} from DLQ`; + ind.classList.add("active"); + setTimeout(() => ind.classList.remove("active"), 4000); + loadOrders(); + } + } catch (e) { /* ignore */ } + } + + document.getElementById("auto-resume-dlq").addEventListener("change", function () { + if (this.checked) { + replayDlq(); + dlqTimer = setInterval(replayDlq, 5000); + } else { + clearInterval(dlqTimer); + dlqTimer = null; + } + }); + checkChaos(); setInterval(checkChaos, 5000); loadOrders(); From 900d89bb2f7dafbedd079de41957de2c092e7f60 Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Sat, 18 Apr 2026 17:09:11 +0200 Subject: [PATCH 19/27] Add collapsible info panel with architecture and chaos engineering notes Co-Authored-By: Claude Sonnet 4.6 --- 01-serverless-app/website/index.html | 103 +++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/01-serverless-app/website/index.html b/01-serverless-app/website/index.html index 226fb1e..6122465 100644 --- a/01-serverless-app/website/index.html +++ b/01-serverless-app/website/index.html @@ -159,6 +159,60 @@ #api-bar { font-size: 0.78rem; color: #aaa; margin-bottom: 1rem; } #api-bar code { background: #f0f0f0; padding: 0.1rem 0.4rem; border-radius: 4px; } + details.info-panel { + background: #fff; + border-radius: 8px; + box-shadow: 0 1px 4px rgba(0,0,0,0.08); + margin-bottom: 1.5rem; + overflow: hidden; + } + details.info-panel summary { + padding: 0.9rem 1.5rem; + cursor: pointer; + font-size: 0.875rem; + font-weight: 600; + color: #444; + text-transform: uppercase; + letter-spacing: 0.05em; + list-style: none; + display: flex; + align-items: center; + gap: 0.5rem; + user-select: none; + } + details.info-panel summary::-webkit-details-marker { display: none; } + details.info-panel summary::before { + content: "›"; + font-size: 1rem; + color: #aaa; + transition: transform 0.15s; + display: inline-block; + } + details.info-panel[open] summary::before { transform: rotate(90deg); } + .info-body { + padding: 0 1.5rem 1.5rem; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1.25rem; + border-top: 1px solid #f0f0f0; + padding-top: 1.25rem; + } + .info-section h3 { font-size: 0.8rem; font-weight: 700; color: #1a1a2e; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; } + .info-section p, .info-section li { font-size: 0.82rem; color: #555; line-height: 1.55; } + .info-section ul { padding-left: 1.1rem; } + .info-section li { margin-bottom: 0.2rem; } + .service-tag { + display: inline-block; + background: #eef2ff; + color: #3730a3; + border-radius: 4px; + padding: 0.1rem 0.45rem; + font-size: 0.72rem; + font-weight: 600; + margin: 0.15rem 0.1rem; + font-family: monospace; + } + #chaos-banner { display: none; background: #fff3cd; @@ -184,6 +238,55 @@

Order Processing Pipeline

API endpoint:

+
+ About this demo +
+
+

What is this?

+

A fully local serverless order-processing pipeline running on + LocalStack — an AWS emulator that lets you develop + and test cloud applications on your laptop without touching a real AWS account.

+

Everything here — Lambda, API Gateway, DynamoDB, + SQS, Step Functions, S3 — runs locally inside a single Docker container.

+
+
+

Architecture

+
    +
  • API Gateway exposes POST /orders and GET /orders
  • +
  • Lambda order-handler writes the order to DynamoDB and enqueues it on SQS
  • +
  • SQS orders-queue buffers orders; failed messages go to a orders-dlq
  • +
  • Lambda order-processor starts a Step Functions execution per order
  • +
  • Step Functions orchestrates: Validate → Wait → Payment → Wait → Fulfill (with error catch)
  • +
  • DynamoDB tracks order status and step timestamps
  • +
  • S3 stores a receipt JSON when an order is fulfilled
  • +
+
+
+

Chaos Engineering

+

Use make inject-fault to enable a + ProvisionedThroughputExceededException + on every DynamoDB UpdateItem call. + This simulates a DynamoDB throttling incident:

+
    +
  • New orders get stuck in pending — the processor Lambda throws before starting the state machine
  • +
  • SQS retries the message 3 times, then routes it to the DLQ
  • +
  • Run make remove-fault to restore normal operation
  • +
  • Enable Auto-resume DLQ above to automatically replay stranded messages once the fault is cleared
  • +
+
+
+

Try it out

+
    +
  • Place an order and watch it move through the pipeline in real time
  • +
  • Inject a fault, place more orders, observe them pile up in pending
  • +
  • Check the chaos banner — it reflects the live fault list from LocalStack
  • +
  • Remove the fault and tick Auto-resume DLQ — orders recover automatically
  • +
  • Expand any row to see per-step timestamps and details
  • +
+
+
+
+
⚠ Chaos mode active — fault injections are enabled, orders may fail or get stuck.
From 751cc16862a26e20c3f168cae4e13fb0442bc325 Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Sat, 18 Apr 2026 17:14:22 +0200 Subject: [PATCH 20/27] Restructure UI: three-column layout with left nav and right sidebar - Left nav: Orders / About sections - Right sidebar: chaos mode toggle, DLQ auto-resume, API endpoint - About section: architecture diagram, service breakdown, chaos walkthrough - Chaos mode toggle directly calls LocalStack chaos API from the browser Co-Authored-By: Claude Sonnet 4.6 --- 01-serverless-app/website/index.html | 708 +++++++++++++++++---------- 1 file changed, 443 insertions(+), 265 deletions(-) diff --git a/01-serverless-app/website/index.html b/01-serverless-app/website/index.html index 6122465..22f49c1 100644 --- a/01-serverless-app/website/index.html +++ b/01-serverless-app/website/index.html @@ -24,8 +24,56 @@ header h1 { font-size: 1.2rem; font-weight: 600; } header span { font-size: 0.85rem; color: #aaa; margin-left: auto; } - main { max-width: 1000px; margin: 2rem auto; padding: 0 1.5rem; } + /* ── Three-column layout ── */ + .page-layout { + max-width: 1280px; + margin: 2rem auto; + padding: 0 1.5rem; + display: grid; + grid-template-columns: 160px 1fr 260px; + gap: 1.5rem; + align-items: start; + } + @media (max-width: 960px) { + .page-layout { grid-template-columns: 1fr 240px; } + .left-nav { display: none; } + } + @media (max-width: 680px) { + .page-layout { grid-template-columns: 1fr; } + .sidebar { display: none; } + } + /* ── Left nav ── */ + .left-nav { + position: sticky; + top: 1.5rem; + } + .nav-label { + font-size: 0.68rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #bbb; + padding: 0 0.5rem 0.5rem; + } + .nav-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.55rem 0.75rem; + border-radius: 6px; + cursor: pointer; + font-size: 0.875rem; + color: #555; + user-select: none; + transition: background 0.1s, color 0.1s; + margin-bottom: 0.15rem; + } + .nav-item:hover { background: #ebebeb; color: #1a1a1a; } + .nav-item.active { background: #e8412a; color: #fff; font-weight: 600; } + .nav-item .nav-icon { font-size: 1rem; } + + /* ── Cards ── */ .card { background: #fff; border-radius: 8px; @@ -33,50 +81,135 @@ padding: 1.5rem; margin-bottom: 1.5rem; } - .card h2 { font-size: 1rem; font-weight: 600; margin-bottom: 1rem; color: #444; text-transform: uppercase; letter-spacing: 0.05em; } + .card:last-child { margin-bottom: 0; } + .card h2 { + font-size: 0.78rem; + font-weight: 700; + margin-bottom: 1rem; + color: #888; + text-transform: uppercase; + letter-spacing: 0.07em; + } + + /* ── Sections ── */ + .section { display: none; } + .section.active { display: block; } + + /* ── About page ── */ + .about-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 1.25rem; + } + .info-section h3 { + font-size: 0.75rem; font-weight: 700; color: #1a1a2e; + text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 0.6rem; + } + .info-section p, .info-section li { font-size: 0.84rem; color: #555; line-height: 1.6; } + .info-section ul { padding-left: 1.1rem; } + .info-section li { margin-bottom: 0.3rem; } + .svc { + display: inline-block; + background: #eef2ff; color: #3730a3; + border-radius: 4px; padding: 0.05rem 0.4rem; + font-size: 0.7rem; font-weight: 700; + margin: 0.1rem; font-family: monospace; + } + .arch-diagram { + background: #f8f9ff; + border: 1px solid #e0e8ff; + border-radius: 8px; + padding: 1rem 1.25rem; + font-size: 0.78rem; + font-family: monospace; + color: #444; + line-height: 1.9; + white-space: pre; + overflow-x: auto; + } + + /* ── Sidebar ── */ + .sidebar { + position: sticky; + top: 1.5rem; + } + .sidebar .card { margin-bottom: 1rem; padding: 1.1rem 1.2rem; } + .sidebar .card h2 { margin-bottom: 0.75rem; } + .sidebar-hint { font-size: 0.76rem; color: #999; line-height: 1.5; margin-top: 0.6rem; } + .sidebar-hint code { font-size: 0.72rem; background: #f0f0f0; padding: 0.05rem 0.3rem; border-radius: 3px; } + + /* ── Chaos toggle ── */ + .chaos-row { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; } + .chaos-status { font-size: 0.875rem; font-weight: 600; } + .chaos-status.on { color: #c0392b; } + .chaos-status.off { color: #888; } + + .toggle-switch { position: relative; display: inline-block; width: 40px; height: 22px; flex-shrink: 0; } + .toggle-switch input { opacity: 0; width: 0; height: 0; } + .toggle-slider { + position: absolute; inset: 0; + background: #ccc; border-radius: 22px; cursor: pointer; transition: background 0.2s; + } + .toggle-slider::before { + content: ""; position: absolute; + width: 16px; height: 16px; left: 3px; top: 3px; + background: #fff; border-radius: 50%; transition: transform 0.2s; + } + .toggle-switch input:checked + .toggle-slider { background: #e8412a; } + .toggle-switch input:checked + .toggle-slider::before { transform: translateX(18px); } + + .fault-list { + margin-top: 0.6rem; + background: #fdf3f2; border: 1px solid #f5c6c2; + border-radius: 6px; padding: 0.5rem 0.65rem; + font-size: 0.7rem; font-family: monospace; color: #922; + white-space: pre-wrap; word-break: break-all; + } + /* ── DLQ card ── */ + .dlq-row { display: flex; align-items: center; justify-content: space-between; } + .dlq-label { + display: flex; align-items: center; gap: 0.4rem; + font-size: 0.875rem; font-weight: 500; color: #444; + cursor: pointer; user-select: none; + } + .dlq-label input { cursor: pointer; } + #dlq-status { font-size: 0.75rem; color: #1a7a2a; font-weight: 600; display: none; } + #dlq-status.active { display: block; margin-top: 0.4rem; } + + /* ── API card ── */ + #api-url-display { + font-family: monospace; font-size: 0.7rem; color: #555; + word-break: break-all; line-height: 1.5; + } + + /* ── Order form ── */ form { display: flex; gap: 0.75rem; align-items: flex-end; flex-wrap: wrap; } label { display: flex; flex-direction: column; gap: 0.3rem; font-size: 0.875rem; color: #555; } input[type="text"], input[type="number"] { - padding: 0.5rem 0.75rem; - border: 1px solid #ddd; - border-radius: 6px; - font-size: 0.9rem; - width: 200px; + padding: 0.5rem 0.75rem; border: 1px solid #ddd; + border-radius: 6px; font-size: 0.9rem; width: 200px; } input[type="number"] { width: 80px; } button[type="submit"] { - padding: 0.5rem 1.25rem; - background: #e8412a; - color: #fff; - border: none; - border-radius: 6px; - font-size: 0.9rem; - cursor: pointer; - font-weight: 500; + padding: 0.5rem 1.25rem; background: #e8412a; color: #fff; + border: none; border-radius: 6px; font-size: 0.9rem; cursor: pointer; font-weight: 500; } button[type="submit"]:hover { background: #c73520; } button[type="submit"]:disabled { background: #ccc; cursor: not-allowed; } - .flash { - padding: 0.6rem 1rem; - border-radius: 6px; - font-size: 0.875rem; - margin-top: 0.75rem; - display: none; + padding: 0.6rem 1rem; border-radius: 6px; + font-size: 0.875rem; margin-top: 0.75rem; display: none; } .flash.success { background: #e6f4ea; color: #1e7e34; display: block; } .flash.error { background: #fde8e8; color: #c0392b; display: block; } + /* ── Orders table ── */ .table-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; } .table-header-right { display: flex; align-items: center; gap: 0.75rem; } .refresh-btn { - padding: 0.3rem 0.9rem; - border: 1px solid #ddd; - border-radius: 6px; - background: #fff; - cursor: pointer; - font-size: 0.85rem; + padding: 0.3rem 0.9rem; border: 1px solid #ddd; + border-radius: 6px; background: #fff; cursor: pointer; font-size: 0.85rem; } .refresh-btn:hover { background: #f5f5f5; } #refresh-indicator { font-size: 0.78rem; color: #aaa; display: none; } @@ -96,14 +229,9 @@ tr.data-row.expanded .expand-icon { transform: rotate(90deg); } .badge { - display: inline-block; - padding: 0.2rem 0.6rem; - border-radius: 99px; - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.04em; - white-space: nowrap; + display: inline-block; padding: 0.2rem 0.6rem; + border-radius: 99px; font-size: 0.75rem; font-weight: 600; + text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap; } .badge.pending { background: #fff3cd; color: #856404; } .badge.validating { background: #cfe2ff; color: #084298; } @@ -111,18 +239,8 @@ .badge.fulfilled { background: #d1f5d3; color: #1a7a2a; } .badge.failed { background: #fde8e8; color: #c0392b; } - .pipeline { - display: flex; - align-items: center; - font-size: 0.7rem; - white-space: nowrap; - } - .pipeline-step { - padding: 0.15rem 0.5rem; - border-radius: 3px; - color: #999; - background: #f0f0f0; - } + .pipeline { display: flex; align-items: center; font-size: 0.7rem; white-space: nowrap; } + .pipeline-step { padding: 0.15rem 0.5rem; border-radius: 3px; color: #999; background: #f0f0f0; } .pipeline-step.done { background: #d1f5d3; color: #1a7a2a; } .pipeline-step.active { background: #cfe2ff; color: #084298; font-weight: 600; } .pipeline-step.failed { background: #fde8e8; color: #c0392b; } @@ -131,101 +249,16 @@ .detail-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 0.5rem 1.5rem; - font-size: 0.82rem; + gap: 0.5rem 1.5rem; font-size: 0.82rem; } .detail-item { display: flex; flex-direction: column; gap: 0.1rem; } .detail-label { font-size: 0.72rem; color: #888; text-transform: uppercase; letter-spacing: 0.05em; } .detail-value { font-family: monospace; color: #333; word-break: break-all; } .detail-value.ts { font-family: inherit; color: #555; } - .order-id { font-family: monospace; font-size: 0.78rem; color: #888; } + .order-id { font-family: monospace; font-size: 0.78rem; color: #888; } .timestamp { font-size: 0.78rem; color: #999; white-space: nowrap; } - .empty { text-align: center; color: #aaa; padding: 2rem; } - - .dlq-label { - display: flex; - align-items: center; - gap: 0.35rem; - font-size: 0.82rem; - color: #555; - cursor: pointer; - user-select: none; - } - .dlq-label input { cursor: pointer; } - #dlq-indicator { font-size: 0.78rem; color: #1a7a2a; display: none; } - #dlq-indicator.active { display: inline; } - - #api-bar { font-size: 0.78rem; color: #aaa; margin-bottom: 1rem; } - #api-bar code { background: #f0f0f0; padding: 0.1rem 0.4rem; border-radius: 4px; } - - details.info-panel { - background: #fff; - border-radius: 8px; - box-shadow: 0 1px 4px rgba(0,0,0,0.08); - margin-bottom: 1.5rem; - overflow: hidden; - } - details.info-panel summary { - padding: 0.9rem 1.5rem; - cursor: pointer; - font-size: 0.875rem; - font-weight: 600; - color: #444; - text-transform: uppercase; - letter-spacing: 0.05em; - list-style: none; - display: flex; - align-items: center; - gap: 0.5rem; - user-select: none; - } - details.info-panel summary::-webkit-details-marker { display: none; } - details.info-panel summary::before { - content: "›"; - font-size: 1rem; - color: #aaa; - transition: transform 0.15s; - display: inline-block; - } - details.info-panel[open] summary::before { transform: rotate(90deg); } - .info-body { - padding: 0 1.5rem 1.5rem; - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 1.25rem; - border-top: 1px solid #f0f0f0; - padding-top: 1.25rem; - } - .info-section h3 { font-size: 0.8rem; font-weight: 700; color: #1a1a2e; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; } - .info-section p, .info-section li { font-size: 0.82rem; color: #555; line-height: 1.55; } - .info-section ul { padding-left: 1.1rem; } - .info-section li { margin-bottom: 0.2rem; } - .service-tag { - display: inline-block; - background: #eef2ff; - color: #3730a3; - border-radius: 4px; - padding: 0.1rem 0.45rem; - font-size: 0.72rem; - font-weight: 600; - margin: 0.15rem 0.1rem; - font-family: monospace; - } - - #chaos-banner { - display: none; - background: #fff3cd; - border: 1px solid #ffc107; - border-radius: 8px; - padding: 0.75rem 1rem; - margin-bottom: 1.5rem; - font-size: 0.875rem; - color: #856404; - } - #chaos-banner.active { display: block; } - #chaos-banner strong { font-weight: 700; } - #chaos-faults { font-family: monospace; font-size: 0.8rem; margin-top: 0.4rem; white-space: pre-wrap; } + .empty { text-align: center; color: #aaa; padding: 2rem; } @@ -235,118 +268,243 @@

Order Processing Pipeline

LocalStack Workshop -
-

API endpoint:

- -
- About this demo -
-
-

What is this?

-

A fully local serverless order-processing pipeline running on - LocalStack — an AWS emulator that lets you develop - and test cloud applications on your laptop without touching a real AWS account.

-

Everything here — Lambda, API Gateway, DynamoDB, - SQS, Step Functions, S3 — runs locally inside a single Docker container.

+
+ + + + + +
+ + +
+
+

New Order

+
+ + + +
+
+
+ +
+
+

Orders

+
+ ↻ auto-refreshing… + +
+
+ + + + + + + + + + + + + + + +
Order IDItemQtyCreatedStatusPipeline
Loading…
-
-

Architecture

-
    -
  • API Gateway exposes POST /orders and GET /orders
  • -
  • Lambda order-handler writes the order to DynamoDB and enqueues it on SQS
  • -
  • SQS orders-queue buffers orders; failed messages go to a orders-dlq
  • -
  • Lambda order-processor starts a Step Functions execution per order
  • -
  • Step Functions orchestrates: Validate → Wait → Payment → Wait → Fulfill (with error catch)
  • -
  • DynamoDB tracks order status and step timestamps
  • -
  • S3 stores a receipt JSON when an order is fulfilled
  • -
+
+ + +
+
+

What is this?

+

+ A fully local serverless order-processing pipeline running on + LocalStack — an AWS cloud emulator that lets you develop + and test cloud-native applications on your laptop without touching a + real AWS account. Every service runs inside a single Docker container. +

+
Browser → API Gateway → Lambda (order-handler) + ↓ ↓ + DynamoDB SQS queue + ↓ + Lambda (order-processor) + ↓ + Step Functions state machine + ┌──────────┬──────────┐ + Validate Payment Fulfill + ↓ ↓ ↓ + DynamoDB DynamoDB S3 + (status) (status) (receipt)
-
-

Chaos Engineering

-

Use make inject-fault to enable a - ProvisionedThroughputExceededException - on every DynamoDB UpdateItem call. - This simulates a DynamoDB throttling incident:

-
    -
  • New orders get stuck in pending — the processor Lambda throws before starting the state machine
  • -
  • SQS retries the message 3 times, then routes it to the DLQ
  • -
  • Run make remove-fault to restore normal operation
  • -
  • Enable Auto-resume DLQ above to automatically replay stranded messages once the fault is cleared
  • -
+ +
+

Architecture

+
+
+

API Layer

+
    +
  • API Gateway custom ID workshop — exposes POST /orders, GET /orders, POST /orders/replay
  • +
  • Lambda order-handler validates input, persists to DynamoDB, enqueues on SQS
  • +
+
+
+

Processing

+
    +
  • SQS orders-queue decouples ingestion from processing; orders-dlq catches failures after 3 retries
  • +
  • Lambda order-processor consumes SQS and starts a Step Functions execution per order
  • +
  • Step Functions orchestrates: Validate → Wait(3s) → Payment → Wait(3s) → Fulfill, with error catch on every step
  • +
+
+
+

Storage

+
    +
  • DynamoDB orders table — tracks order status and per-step timestamps (validating_at, payment_at, …)
  • +
  • S3 order-receipts bucket — stores a receipt JSON at receipts/<order_id>.json on fulfillment
  • +
+
+
-
-

Try it out

-
    -
  • Place an order and watch it move through the pipeline in real time
  • -
  • Inject a fault, place more orders, observe them pile up in pending
  • -
  • Check the chaos banner — it reflects the live fault list from LocalStack
  • -
  • Remove the fault and tick Auto-resume DLQ — orders recover automatically
  • -
  • Expand any row to see per-step timestamps and details
  • -
+ +
+

Chaos Engineering

+
+
+

What we inject

+

Toggle Chaos Mode in the sidebar to inject a + ProvisionedThroughputExceededException + on every DynamoDB UpdateItem call via + the LocalStack Chaos API (POST /_localstack/chaos/faults).

+
+
+

What breaks

+
    +
  • The order-processor Lambda tries to update order status before starting the state machine
  • +
  • DynamoDB throws → Lambda fails → SQS retries the message 3×
  • +
  • After 3 failures the message is routed to the DLQ
  • +
  • Order stays stuck in pending indefinitely
  • +
+
+
+

Recovery

+
    +
  • Disable chaos mode to restore normal DynamoDB behaviour
  • +
  • Enable Auto-resume DLQ — the UI polls POST /orders/replay every 5 s and re-queues stranded messages
  • +
  • Orders resume processing through the full pipeline
  • +
+
+
+
+ +
+

Try it out

+
+
+

Happy path

+
    +
  • Go to Orders and place an order
  • +
  • Watch the status badge and pipeline bar update in real time
  • +
  • Expand the row to see per-step timestamps
  • +
+
+
+

Chaos scenario

+
    +
  • Enable Chaos Mode in the sidebar
  • +
  • Place one or more orders — they pile up in pending
  • +
  • Disable chaos, tick Auto-resume DLQ
  • +
  • Watch orders automatically recover and fulfill
  • +
+
+
+
+
+ +
+ + +
- -
- ⚠ Chaos mode active — fault injections are enabled, orders may fail or get stuck. -
-
- -
-

New Order

-
- - - -
-
-
- -
-
-

Orders

-
- ↺ replayed from DLQ + +
+

DLQ Recovery

+
- ↻ auto-refreshing… -
+
+
- - - - - - - - - - - - - - - -
Order IDItemQtyCreatedStatusPipeline
Loading…
-
-
+ +
+

API Endpoint

+ +
+ + + +