The whole-stack synthesis chapter. Everything from chapters 2-9 (IAM, VPC, Lambda, S3, DynamoDB, SQS, KMS, alarms) comes back together as a working serverless system: API Gateway in front, Lambda in the middle, DynamoDB + EventBridge behind, Step Functions tying it all up. By the end you'll know when to reach for Lambda vs Step Functions, and which of the four API Gateway flavors to pick.
Azure Functions, APIM, Logic Apps, Durable Functions and Cosmos-triggered Functions all roughly translate to AWS - but not 1-to-1. AWS splits the same conceptual surface across more pieces (Lambda, two flavors of API Gateway, Step Functions Standard vs Express, DynamoDB Streams, EventBridge, SQS event source mapping) and asks you to wire them yourself. That extra wiring is the cost; the benefit is that each piece scales independently and you pay only for what fires.
| Concept | Azure | AWS |
|---|---|---|
| Function-as-a-service | Azure Functions (Consumption / Premium / Flex) | AWS Lambda (on-demand / provisioned concurrency) |
| HTTP API in front | API Management (APIM) or Functions HTTP trigger | API Gateway (HTTP API for most cases; REST API for advanced features) |
| Visual workflow / orchestration | Logic Apps (low-code) or Durable Functions (code) | Step Functions Standard (1yr max, $25/M state transitions) |
| High-throughput workflow | Durable Functions (no explicit Express tier) | Step Functions Express (5 min max, billed per-ms) |
| DB change feed → function | Cosmos DB change feed + Functions trigger | DynamoDB Streams + Lambda event source mapping |
| Queue → function | Service Bus / Storage Queue binding | SQS event source mapping (poll-based) or EventBridge Pipe |
| Event bus | Event Grid topics + subscriptions | EventBridge (default bus + custom buses + Pipes) |
| Long-poll / streaming events | Event Hubs + Functions trigger | Kinesis Data Streams + Lambda |
| Scheduled trigger | Functions Timer trigger (NCRONTAB) | EventBridge Scheduler (cron + rate; replaces CW Events Rules) |
| Container-image-as-function | Functions custom image (Premium plan) | Lambda container images (up to 10 GB) |
| Edge compute | Azure Front Door rules + Functions on Static Web Apps | Lambda@Edge (CloudFront) or CloudFront Functions (lighter) |
| Serverless framework | Bicep / azd / Pulumi | SAM (Serverless Application Model), Serverless Framework, CDK, Terraform |
| Cold start mitigation | Premium plan "always-ready" instances | Provisioned Concurrency (pre-warmed) or SnapStart (Java/Python) |
sam local invoke runs the function in a Docker container; sam deploy wraps CloudFormation. Useful for quick local iteration; production teams typically migrate to CDK or Terraform.Chapter 4 covered Lambda's basic shape. Here we look at the four knobs that determine real-world cost and latency: cold starts, concurrency model, CPU architecture, and packaging.
The default. Lambda scales out to whatever load arrives, up to your account's per-region concurrency limit (1,000 by default; raise via support). Each new concurrent invocation may pay a cold start.
Cost: pay only for actual ms billed.
Pre-warmed copies. You declare "keep 10 copies always ready". Cold starts are eliminated for those 10 slots; the 11th concurrent invocation still pays one. Useful for latency-sensitive APIs.
Cost: hourly rate per provisioned copy plus the regular invoke ms.
A cap, not a floor. Sets the maximum concurrent executions for this function. Protects downstream systems (DynamoDB, RDS) from a runaway Lambda. Also subtracts from the account-wide pool.
Cost: free; pure limit.
Lambda runs on either x86_64 or arm64. ARM (Graviton2) costs ~20% less per ms, often performs comparably or better, and is a one-line config change for most runtimes (Python, Node, Java, Ruby). If your code has no native x86 binaries, switch.
resource "aws_lambda_function" "api" {
function_name = "reports-api"
role = aws_iam_role.lambda.arn
handler = "index.handler"
runtime = "python3.12"
filename = "build.zip"
memory_size = 512
timeout = 10
# Default = x86_64
# Costs: $0.0000166667 / GB-second
}
resource "aws_lambda_function" "api" {
function_name = "reports-api"
role = aws_iam_role.lambda.arn
handler = "index.handler"
runtime = "python3.12"
filename = "build.zip"
memory_size = 512
timeout = 10
architectures = ["arm64"] # ~20% cheaper
# Costs: $0.0000133334 / GB-second
}
architectures = ["x86_64"]. If you have time to do exactly one cost-optimization across your serverless stack, this is the one with the highest ROI.
Until 2020 a Lambda deployment package was a ZIP, max 250 MB unzipped. Now you can also publish a Lambda as an OCI container image, up to 10 GB. The image has to use one of AWS's base images (which include the Lambda Runtime API client). Use this for ML inference, large dependency trees (think pandas + scikit), or when you want the same image to run in Lambda and ECS/EKS.
FROM public.ecr.aws/lambda/python:3.12
COPY requirements.txt ${LAMBDA_TASK_ROOT}/
RUN pip install -r requirements.txt
COPY app.py ${LAMBDA_TASK_ROOT}/
# Handler in <file>.<function> form
CMD [ "app.handler" ]
Two flavors of running code at the CDN edge:
| Capability | CloudFront Functions | Lambda@Edge |
|---|---|---|
| Runtime | JavaScript (cfjs runtime) | Node.js or Python |
| Max exec time | 1 ms | 5 sec (viewer events) or 30 sec (origin events) |
| Memory | 2 MB | up to 10 GB |
| Network calls | None - pure compute | Full SDK, can call other AWS services |
| Cost | $0.10 / million requests | $0.60 / million + $0.00005 / GB-s |
| Use cases | Header rewrites, URL normalization, auth-cookie checks | Personalization, A/B routing, SSR, image resize |
The takeaway: if your edge logic fits in 1 ms of pure JS, CloudFront Functions is 6x cheaper and lives even closer to the user. Reach for Lambda@Edge only when you need real compute or AWS SDK access at the edge.
"API Gateway" is one product name with three SKUs. They're not feature-equivalent and they're not priced equivalent. Picking is the most consequential decision in any new serverless API.
| Authorizer type | HTTP API | REST API | Used for |
|---|---|---|---|
| None | yes | yes | Public endpoints (you handle auth yourself) |
| JWT (built-in) | yes | no | Verify JWTs from any OIDC provider (Auth0, Okta, Cognito, Azure AD) |
| Cognito user pool | via JWT | yes | AWS-native user auth |
| Lambda authorizer | yes (simple/IAM) | yes (token/request) | Custom logic - you write a Lambda that returns Allow/Deny |
| AWS_IAM (SigV4) | yes | yes | Internal service-to-service, callers sign with IAM credentials |
| API key | no | yes | Throttling / metering per consumer (usage plans) |
| mTLS | yes | yes | Client-certificate auth (B2B integrations) |
A team builds a new public API for a partner integration. They click through the API Gateway console - the first option is "REST API", so they pick it. The API ships, hits ~100M requests/month at steady state. JWT auth from the partner's Okta. No request validation, no caching, no API keys. After three months a FinOps review flags $350/month on API Gateway alone. Why is the bill so high, and what's the easy fix?
The math: 100M req/month × $3.50/M = $350/month = $4,200/year. The same workload on HTTP API: 100M × $1.00/M = $100/month = $1,200/year. Savings: $3,000/year for every 100M requests, just by picking the right SKU.
What the team gave up (and didn't use): API keys / usage plans (they use JWT instead), request schema validation (they validate in the Lambda), built-in caching (their Lambda is already fast enough), direct WAF integration (they put CloudFront in front instead), private endpoints (their API is public).
The reason teams pick REST API by default is that the console shows it first. The reason they should pick HTTP API is that the console's order doesn't reflect 2026's best practice. Fix: rebuild as HTTP API in a new stage, run both side-by-side, swap the DNS, decommission REST.
Caveat: if you genuinely need API keys (e.g., metered B2B billing) or request validation, REST API earns its 3.5x. Only ~10% of new APIs actually do.
An API Gateway "integration" is the thing your route forwards to. The common ones:
| Integration | What happens | When to use |
|---|---|---|
| Lambda proxy | The whole HTTP request is JSON-ified and handed to Lambda; Lambda returns a JSON shape that becomes the HTTP response. | Default for ~all serverless APIs. Both flavors support it. |
| AWS service direct | API Gateway calls another AWS service (DynamoDB, S3, SQS) directly without a Lambda in between. | Simple CRUD where Lambda would just be a pass-through. Skip the cold start. |
| HTTP proxy | Forwards to an arbitrary URL. | Front an internal ALB / on-prem service through API Gateway for auth + logging. |
| VPC Link | Calls a private resource (ALB, NLB) inside your VPC. | Expose an internal microservice publicly. |
| MOCK | API Gateway returns a canned response, no backend. | Health endpoints, stubs during development. |
A teammate set up an async-invoked Lambda (e.g., triggered by S3 events). To debug failures they configured a Lambda destination = an SQS queue for the OnFailure case. They expected failed invocations' event payloads to land in that queue so they could replay them. Nothing arrives. The Lambda is failing (CloudWatch shows errors), but the SQS queue stays empty. Below is the function's execution-role policy and the destination config.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::raw-events/*"
}
]
}
aws lambda put-function-event-invoke-config \
--function-name process-event \
--destination-config '{
"OnFailure": {
"Destination": "arn:aws:sqs:us-east-1:111111111111:event-dlq"
}
}'
sqs:SendMessage on the failure-destination queue.
Lambda destinations are sent using the function's execution role. When the role doesn't have sqs:SendMessage on the target queue's ARN, Lambda silently drops the destination message - the invocation already failed, the user code already returned, so the "delivery to destination failed" is just a log entry buried in CloudWatch. No alarm, no retry, no exception bubbling up.
Fix: add this statement to the execution role:
{
"Effect": "Allow",
"Action": "sqs:SendMessage",
"Resource": "arn:aws:sqs:us-east-1:111111111111:event-dlq"
}
Also: enable an alarm on the DestinationDeliveryFailures CloudWatch metric for every Lambda you configure destinations on. That's the canary you wish you'd had.
A Step Functions state machine is a JSON document describing a workflow: states, transitions, parallel branches, error handling, retries. Each state typically invokes a Lambda (or any of 200+ AWS services). The Step Functions service holds the running state of every workflow execution.
1-year max duration. Exactly-once execution. Full visual history of every state transition in the console. Billed per state transition: $25 / million transitions.
Use for: human approval flows, multi-day data pipelines, anything you need to audit step-by-step.
5-minute max duration. At-least-once execution. No visual replay history (logs go to CloudWatch). Billed per execution + per ms of compute - $1 / million at the small end.
Use for: high-throughput chains (10K+/sec), event-driven flows, API request handlers that need ordering.
Synchronous variant. Same Express engine but the caller waits for the result. Used as the "all-in-one" target for an API Gateway HTTP route: client calls API, API calls Step Functions sync, gets the result, returns it.
Use for: short orchestrations behind an API.
[FunctionName("Orchestrator")]
public static async Task Run(
[OrchestrationTrigger] IDurableOrchestrationContext ctx)
{
var input = ctx.GetInput<Order>();
var tasks = new List<Task>();
foreach (var item in input.Items)
{
tasks.Add(ctx.CallActivityAsync(
"ProcessItem", item));
}
await Task.WhenAll(tasks);
await ctx.CallActivityAsync(
"SendConfirmation", input);
}
{
"Comment": "Process each item, then confirm",
"StartAt": "ProcessItems",
"States": {
"ProcessItems": {
"Type": "Map",
"ItemsPath": "$.items",
"Iterator": {
"StartAt": "ProcessOne",
"States": {
"ProcessOne": {
"Type": "Task",
"Resource": "arn:aws:lambda:...:function:process",
"End": true
}
}
},
"Next": "Confirm"
},
"Confirm": {
"Type": "Task",
"Resource": "arn:aws:lambda:...:function:confirm",
"End": true
}
}
}
Step Functions costs money per transition. For a 3-step linear flow that runs 10M times/month, that's 30M transitions = $750/month on Standard. If the flow doesn't need waits, retries-with-backoff, or visual auditing, just chain the Lambdas yourself via async invoke or EventBridge - the orchestration is free. Use Step Functions when the orchestration is the value, not the glue.
A DynamoDB Stream captures every INSERT, MODIFY, and REMOVE on a table as an ordered, partitioned log. Records live 24 hours. The most common consumer is a Lambda via event source mapping: AWS polls the stream, batches records (default batch size 100), and invokes your Lambda once per batch.
| StreamViewType | What's in each record | When to use |
|---|---|---|
KEYS_ONLY | Just the partition + sort key of the changed item | Cache invalidation - you only need to know which item changed |
NEW_IMAGE | Full new state of the item | Replicating writes downstream (search index, cache populate) |
OLD_IMAGE | Full pre-update state | Audit logs - "what did this look like before?" |
NEW_AND_OLD_IMAGES | Both before and after | Diff-based logic (e.g., "only fire if status changed") |
resource "aws_dynamodb_table" "reports" {
name = "gfn-reports"
billing_mode = "PAY_PER_REQUEST"
hash_key = "report_id"
stream_enabled = true
stream_view_type = "NEW_AND_OLD_IMAGES"
attribute {
name = "report_id"
type = "S"
}
}
resource "aws_lambda_event_source_mapping" "reports_to_search" {
event_source_arn = aws_dynamodb_table.reports.stream_arn
function_name = aws_lambda_function.indexer.arn
starting_position = "LATEST"
batch_size = 100
maximum_batching_window_in_seconds = 5
# Error handling - send poison records to a DLQ instead of stalling the shard
destination_config {
on_failure {
destination_arn = aws_sqs_queue.indexer_dlq.arn
}
}
maximum_retry_attempts = 3
}
maximum_retry_attempts + an on_failure destination - otherwise a single bad record halts the whole consumer.
Here's the canonical "modern serverless" shape, end to end. A client calls an HTTP API. A write Lambda persists to DynamoDB and emits an event to EventBridge. EventBridge routes the event to one or more downstream Lambdas (notifications, analytics, audit log). DynamoDB Streams gives you a second async fan-out path if you need ordered table-change processing.
# 1. The HTTP API itself
resource "aws_apigatewayv2_api" "reports" {
name = "reports-api"
protocol_type = "HTTP"
}
# 2. Lambda integration
resource "aws_apigatewayv2_integration" "write" {
api_id = aws_apigatewayv2_api.reports.id
integration_type = "AWS_PROXY"
integration_uri = aws_lambda_function.write_report.invoke_arn
payload_format_version = "2.0"
}
# 3. Route
resource "aws_apigatewayv2_route" "write" {
api_id = aws_apigatewayv2_api.reports.id
route_key = "POST /reports"
target = "integrations/${aws_apigatewayv2_integration.write.id}"
}
# 4. EventBridge custom bus
resource "aws_cloudwatch_event_bus" "reports" {
name = "reports-bus"
}
# 5. Rule: route published-status events to notify-slack Lambda
resource "aws_cloudwatch_event_rule" "published" {
name = "report-published"
event_bus_name = aws_cloudwatch_event_bus.reports.name
event_pattern = jsonencode({
source = ["reports.api"]
detail-type = ["ReportPublished"]
})
}
resource "aws_cloudwatch_event_target" "slack" {
rule = aws_cloudwatch_event_rule.published.name
event_bus_name = aws_cloudwatch_event_bus.reports.name
arn = aws_lambda_function.notify_slack.arn
}
Sketched, not complete (the IAM roles, permissions, Lambda functions, and EventBridge-to-Lambda permissions are omitted here). Chapter 12 builds a similar stack as a complete Terraform module.
InvocationType=Event. If A genuinely needs B's result, ask whether the two should be one function.
A team set up an S3-events-trigger-Lambda pipeline for image processing: drop a JPG into raw-bucket, a Lambda reads it, generates thumbnails, writes them to the same bucket under a thumbs/ prefix. The S3 event notification was configured with no prefix filter. No reserved concurrency on the function. No alarms beyond basic error rates.
A user uploaded a batch of 2,000 photos. The Lambda fired 2,000 times in parallel and wrote 6,000 thumbnails (small, medium, large). Each thumbnail-write was itself an S3 event, which triggered the Lambda again - which wrote three more thumbnails of each thumbnail. By minute 5, concurrent executions hit the account-wide cap of 10,000. The DynamoDB metadata table (provisioned capacity, 100 WCU) was being slammed by every invocation; throttles cascaded. The downstream notification SNS topic hit its publish quota; failures cascaded back to Lambda retries, multiplying load further.
By the time pagers fired and the on-call deleted the event trigger, the bill for the 4-hour window had crossed $15,000 - Lambda compute, S3 PUT requests, DynamoDB throughput overage, CloudWatch Logs ingestion, and SNS publishes combined.
Lessons: (1) Set reserved concurrency on every Lambda - even a small cap (50) would have throttled the recursion to a slow drip. (2) Never let a Lambda's output land in a location that triggers the same Lambda. Use a different bucket / different prefix + S3 event filter. (3) Set a billing alarm at 2x normal daily spend so a runaway gets noticed in minutes, not hours. (4) AWS now offers "Recursive loop detection" for Lambda (auto-stops after 16 cycles), but it's opt-in and post-incident only.
Goal: get a quick survey of what serverless plumbing already exists in the account you're connected to. Three list calls, all free, all read-only.
Step 1. List all Lambda functions and their architecture / memory / runtime.
aws lambda list-functions \
--query 'Functions[].[FunctionName, Runtime, Architectures[0], MemorySize, Timeout]' \
--output table
# Look for: x86_64 functions you could move to arm64,
# oversize MemorySize (most workloads are fine at 512 MB),
# Timeout=900 (the 15-min hard cap - usually means a Step Functions candidate)
Step 2. List API Gateway v2 (HTTP and WebSocket) APIs.
# HTTP and WebSocket APIs
aws apigatewayv2 get-apis \
--query 'Items[].[Name, ProtocolType, ApiEndpoint]' \
--output table
# REST APIs (different SDK namespace, different price tier)
aws apigateway get-rest-apis \
--query 'items[].[name, id, createdDate]' \
--output table
# Any REST APIs you find are 3.5x more expensive than HTTP API equivalents.
# For each one, ask: does it actually use a REST-API-only feature?
Step 3. List Step Functions state machines.
aws stepfunctions list-state-machines \
--query 'stateMachines[].[name, type, creationDate]' \
--output table
# For each one, check the type:
# STANDARD = $25/M transitions, 1-year max
# EXPRESS = $1/M + per-ms compute, 5-min max
# If you find STANDARD machines that finish in seconds and run millions of times,
# EXPRESS is usually 10-100x cheaper.
What you learned: a 30-second sweep with three CLI calls usually surfaces at least one obvious optimization in any non-trivial account: x86 Lambda that could be ARM, REST API that should be HTTP, or Standard workflow that should be Express.
Serverless economics rewards picking the right SKU. Make sure these stick.
1. You're building a new public HTTP API that needs JWT auth. Your traffic is 100M requests/month. What's the rough monthly API Gateway cost difference between REST API and HTTP API?
2. What's Lambda's maximum payload size for synchronous vs asynchronous invocations?
3. You're building a workflow that runs ~10,000 times/second, each execution finishes in under 30 seconds, and you don't need per-execution audit replay. Which Step Functions tier?
4. What's the default per-region Lambda concurrency limit, and what happens at the limit?
TooManyRequestsException (429), async invokes go to the function's queue (up to 6 hours), event source mappings back off. Raise via AWS Support - production accounts often run with 10K-50K. Reserved concurrency on a function carves out a guaranteed slice from this pool; provisioned concurrency doesn't change the cap but pre-warms slots.
5. What's the default API Gateway throttling limit for a new account, per region?
429 Too Many Requests. You can set per-stage, per-route, or per-API-key (REST API only) throttles below this account-wide cap, and request a raise via Support. The defaults are generous enough that most teams never need to think about it - but a viral launch can hit it.
timeout is 900 seconds. If your workload routinely runs longer, Lambda is the wrong tool - use Step Functions to chain shorter Lambdas, or move to ECS Fargate / EC2. Azure Functions Premium / Consumption has the same kind of ceiling (10 min on Consumption) but Durable Functions can run indefinitely; the AWS equivalent for indefinite duration is Step Functions Standard (up to 1 year), not Lambda itself.
504 while the Lambda keeps executing and billing. Anything longer = async pattern (return 202, write to SQS/EventBridge, client polls or webhooks).
Picking up from chapter 10 where the worker moved to EKS via IRSA. The reports table already exists (chapter 6). Now we expose a query API: GET /reports/{report_id} backed by a small Lambda that reads from DynamoDB. We deliberately pick HTTP API over REST API (saving 3.5x cost), and use AWS_IAM authorization since the API is internal-only - callers from inside NVIDIA sign requests with their IAM credentials.
Why now: the table has data, the role exists, IAM is sane. The missing piece is a thin read API for dashboards and other services to query against. Internal-only, IAM-authenticated, HTTP API tier (not REST).
import json, os
import boto3
ddb = boto3.resource("dynamodb").Table(os.environ["TABLE"])
def handler(event, _context):
report_id = event["pathParameters"]["report_id"]
item = ddb.get_item(Key={"report_id": report_id}).get("Item")
if not item:
return {"statusCode": 404, "body": json.dumps({"error": "not found"})}
return {"statusCode": 200, "body": json.dumps(item, default=str)}
# Package and create
zip handler.zip handler.py
aws lambda create-function \
--function-name gfn-reports-query \
--runtime python3.12 \
--architectures arm64 \
--role arn:aws:iam::111111111111:role/gfn-reports-role \
--handler handler.handler \
--zip-file fileb://handler.zip \
--environment 'Variables={TABLE=gfn-reports}' \
--memory-size 256 \
--timeout 5 \
--profile compass
# 1. Create the HTTP API ($1/M, not $3.50/M)
API_ID=$(aws apigatewayv2 create-api \
--name gfn-reports-api \
--protocol-type HTTP \
--target arn:aws:lambda:us-east-1:111111111111:function:gfn-reports-query \
--query ApiId --output text --profile compass)
# 2. Update the auto-created route to require AWS_IAM
ROUTE_ID=$(aws apigatewayv2 get-routes --api-id $API_ID \
--query 'Items[0].RouteId' --output text --profile compass)
aws apigatewayv2 update-route \
--api-id $API_ID --route-id $ROUTE_ID \
--route-key "GET /reports/{report_id}" \
--authorization-type AWS_IAM \
--profile compass
# 3. Allow API Gateway to invoke the Lambda
aws lambda add-permission \
--function-name gfn-reports-query \
--statement-id apigw-invoke \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:us-east-1:111111111111:$API_ID/*" \
--profile compass
# 4. Note the endpoint URL
aws apigatewayv2 get-api --api-id $API_ID \
--query ApiEndpoint --output text --profile compass
# IAM-authed endpoints need SigV4 signing - curl alone won't work.
# Use awscurl (pip install awscurl) or the AWS SDK in any language.
awscurl --service execute-api \
--region us-east-1 \
--profile compass \
"https://${API_ID}.execute-api.us-east-1.amazonaws.com/reports/r-12345"
# Expected: {"report_id": "r-12345", "status": "published", ...}
# Unauthed plain curl would get: {"message":"Missing Authentication Token"}
resource "aws_lambda_function" "query" {
function_name = "gfn-reports-query"
role = aws_iam_role.compass.arn
handler = "handler.handler"
runtime = "python3.12"
architectures = ["arm64"]
filename = "build/handler.zip"
source_code_hash = filebase64sha256("build/handler.zip")
memory_size = 256
timeout = 5
environment {
variables = { TABLE = "gfn-reports" }
}
}
resource "aws_apigatewayv2_api" "reports" {
name = "gfn-reports-api"
protocol_type = "HTTP"
}
resource "aws_apigatewayv2_integration" "query" {
api_id = aws_apigatewayv2_api.reports.id
integration_type = "AWS_PROXY"
integration_uri = aws_lambda_function.query.invoke_arn
payload_format_version = "2.0"
}
resource "aws_apigatewayv2_route" "get_report" {
api_id = aws_apigatewayv2_api.reports.id
route_key = "GET /reports/{report_id}"
target = "integrations/${aws_apigatewayv2_integration.query.id}"
authorization_type = "AWS_IAM" # internal only - callers sign with SigV4
}
resource "aws_apigatewayv2_stage" "default" {
api_id = aws_apigatewayv2_api.reports.id
name = "$default"
auto_deploy = true
}
resource "aws_lambda_permission" "apigw" {
statement_id = "apigw-invoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.query.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.reports.execution_arn}/*"
}
output "api_endpoint" {
value = aws_apigatewayv2_api.reports.api_endpoint
}
Read the two side by side - same end state, but the Terraform version is the one you'd actually keep. Chapter 12 puts this whole stack (plus everything from chapters 2-10) into a versioned module.
403 Forbidden. No tokens to issue, no JWKS to publish, no Cognito user pool to manage. If the API were public (third-party callers) we'd switch to JWT authorizer instead.
| Trap | Fix |
|---|---|
| "My Lambda is slow on the first call" | Cold start. Try ARM64 (smaller container, faster init), provisioned concurrency, or SnapStart for JVM. |
| "My API bill is bigger than I expected" | Check if it's REST API. Move to HTTP API unless you need a REST-only feature. |
| "My event source mapping stalled" | A poison record halted a stream shard. Set maximum_retry_attempts + on_failure destination. |
| "My destinations config silently drops failures" | The execution role is missing sqs:SendMessage (or equivalent) on the destination ARN. |
| "My Lambda recursively triggers itself" | Output bucket = input bucket, no prefix filter, no reserved concurrency. Fix all three. |
terraform apply.