AWS CLI First #15: Serverless from the Terminal: Lambda Functions & HTTP API Gateways
Master serverless architecture entirely from the command line: package deployment artifacts, configure fine-grained IAM execution roles, deploy ARM64 Python/Node.js Lambda functions, and expose ultra-low latency endpoints via HTTP API Gateway v2.
AWS with CLI: The Terminal-First Mastery Path
Part of the CLI-first comprehensive learning path from foundational setup to production cloud architectures.
In previous chapters, we managed compute instances with EC2, auto-scaled them across availability zones with ASGs, and orchestrated container tasks with ECS Fargate. While Fargate abstracts server management, you still manage container lifecycles, memory boundaries, and health check timeouts.
AWS Lambda and Amazon API Gateway v2 (HTTP API) represent the ultimate cloud abstraction: Pure Serverless Computing:
- Zero Idle Cost: When nobody calls your API at 3:00 AM, you pay exactly $0.00. Billing is calculated strictly per millisecond of compute execution.
- Infinite Instant Concurrency: From 1 request per minute to 10,000 requests per second, AWS automatically instantiates isolated microVMs (via Firecracker) to handle incoming requests in parallel.
- HTTP API Gateway v2: Engineered from the ground up to replace legacy v1 REST APIs—offering sub-10ms latency and 70% cost reduction with native CORS and OpenID Connect (OIDC) JWT validation.
In this guide, we master serverless microservice deployment purely from the terminal. We will author an event-driven Python microservice, package deployment zip archives, configure least-privilege IAM execution roles, deploy an ARM64 Graviton Lambda function, wire it to HTTP API Gateway v2, and test live production invocations using curl.
💰 Estimated Lab Cost & Resource Architecture
1. Cost Breakdown for this Lab
All resources in this lab are provisioned on-demand in the Asia Pacific (Singapore) ap-southeast-1 region:
| Resource | Quantity | Unit Cost (Hourly) | Estimated Lab Cost (1 Hour) |
|---|---|---|---|
| AWS Lambda (ARM64 Architecture) | ~100 Invocations (< 100ms) | Free Tier: 1 Million free requests/mo | $0.000 |
| HTTP API Gateway v2 | ~100 API Requests | Free Tier: 1 Million free calls/mo | $0.000 |
| Amazon CloudWatch Logs | < 1 MB Log Data | Free Tier: 5 GB free ingestion | $0.000 |
| Total Estimated Lab Cost | ~$0.000 USD (Free Tier) |
2. Architecture Diagram
+----------------------------------------------------------------+
| Serverless Microservice Architecture |
+-------------------------------+--------------------------------+
|
v
+-------------------------------+--------------------------------+
| Client / Web Browser / curl (HTTPS Request to API) |
+-------------------------------+--------------------------------+
|
v
+-------------------------------+--------------------------------+
| Amazon API Gateway v2 (HTTP API - Low Latency) |
| - Route: GET /api/v1/health |
| - Route: POST /api/v1/orders |
| - CORS: Enabled (*), In-Transit TLS 1.3 Termination |
+-------------------------------+--------------------------------+
|
v (Payload Proxy Integration)
+-------------------------------+--------------------------------+
| AWS Lambda Function (Graviton ARM64) |
| - Runtime: Python 3.12 (Architecture: arm64) |
| - Memory: 128 MB (Billed per 1ms increment) |
| - IAM Role: LambdaBasicExecutionRole (CloudWatch Write) |
+-------------------------------+--------------------------------+
|
v
+----------------------------------------------------------------+
| Telemetry & Observability: |
| - CloudWatch Log Group: /aws/lambda/orders-serverless-api |
| - Structured JSON Logs (RequestId, Duration, Memory Used) |
+----------------------------------------------------------------+
🔐 Phase 1: Serverless IAM Execution Roles
An AWS Lambda function executes inside an isolated microVM sandbox. It cannot interact with any AWS service—not even write logs to CloudWatch—without an explicitly assigned IAM Execution Role.
1. Creating the Lambda Execution Role
We define a trust policy allowing the lambda.amazonaws.com service principal to assume the role:
REGION="ap-southeast-1"
PROFILE="cloud-dev-admin"
ROLE_NAME="OrdersLambdaExecutionRole"
aws iam get-role --role-name "$ROLE_NAME" --profile "$PROFILE" >/dev/null 2>&1 || \
aws iam create-role \
--role-name "$ROLE_NAME" \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}
]
}' \
--description "Least-privilege role for Orders serverless microservice" \
--profile "$PROFILE"
# Attach standard AWS managed basic execution policy (CloudWatch logging)
aws iam attach-role-policy \
--role-name "$ROLE_NAME" \
--policy-arn "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" \
--profile "$PROFILE"
LAMBDA_ROLE_ARN=$(aws iam get-role --role-name "$ROLE_NAME" --profile "$PROFILE" --query "Role.Arn" --output text)
echo "Lambda Execution Role ARN: $LAMBDA_ROLE_ARN"
# Wait 5 seconds for IAM replication
sleep 5
🐍 Phase 2: Authoring & Packaging the Microservice Code
Lambda accepts deployment code as either a container image or a lightweight .zip archive. For ultra-fast cold starts (< 50ms), zip archives are ideal.
1. Authoring the Handler Function (lambda_function.py)
Our Python 3.12 microservice inspects the API Gateway event payload, handles routing, and returns structured JSON responses:
mkdir -p ./lambda-src
cat <<'EOF' > ./lambda-src/lambda_function.py
import json
import os
import time
def lambda_handler(event, context):
http_method = event.get("requestContext", {}).get("http", {}).get("method", "GET")
raw_path = event.get("rawPath", "/")
print(f"[INFO] Received {http_method} request for path: {raw_path}")
# Route: GET /api/v1/health
if raw_path.endswith("/health"):
return {
"statusCode": 200,
"headers": {
"Content-Type": "application/json",
"X-Serverless-Engine": "AWS-Lambda-Graviton"
},
"body": json.dumps({
"status": "HEALTHY",
"service": "orders-serverless-api",
"runtime": "python3.12-arm64",
"timestamp": int(time.time())
})
}
# Route: POST /api/v1/orders
if http_method == "POST" and "orders" in raw_path:
try:
body = json.loads(event.get("body", "{}"))
except Exception:
body = {}
order_id = f"ORD-{int(time.time() * 1000)}"
item = body.get("item", "Standard Cloud Compute")
quantity = body.get("quantity", 1)
return {
"statusCode": 201,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({
"message": "Order processed successfully",
"order_id": order_id,
"item": item,
"quantity": quantity,
"status": "CONFIRMED"
})
}
# Default Fallback Route
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({
"message": "Welcome to AWS Serverless API Gateway v2 + Lambda!",
"available_routes": ["GET /api/v1/health", "POST /api/v1/orders"]
})
}
EOF
2. Creating the Deployment Zip Archive
Package the source directory into an optimized zip file:
cd ./lambda-src
zip -q -r9 ../function.zip lambda_function.py
cd ..
ls -lh function.zip
🚀 Phase 3: Provisioning the Lambda Function on Graviton (ARM64)
We deploy the function using the ARM64 architecture (--architectures arm64), which delivers up to 34% better price-performance compared to legacy x86_64 runtimes:
REGION="ap-southeast-1"
PROFILE="cloud-dev-admin"
FUNCTION_NAME="orders-serverless-api"
# Check if function exists or create fresh
if aws lambda get-function --function-name "$FUNCTION_NAME" --region "$REGION" --profile "$PROFILE" >/dev/null 2>&1; then
echo "Updating existing function code..."
aws lambda update-function-code \
--function-name "$FUNCTION_NAME" \
--zip-file "fileb://function.zip" \
--region "$REGION" \
--profile "$PROFILE" >/dev/null
else
echo "Creating new serverless Lambda function on Graviton ARM64..."
aws lambda create-function \
--function-name "$FUNCTION_NAME" \
--runtime "python3.12" \
--role "$LAMBDA_ROLE_ARN" \
--handler "lambda_function.lambda_handler" \
--architectures "arm64" \
--timeout 10 \
--memory-size 128 \
--zip-file "fileb://function.zip" \
--description "Event-driven orders microservice via AWS CLI" \
--tags "Environment=Production" \
--region "$REGION" \
--profile "$PROFILE" >/dev/null
fi
LAMBDA_ARN=$(aws lambda get-function --function-name "$FUNCTION_NAME" --region "$REGION" --profile "$PROFILE" --query "Configuration.FunctionArn" --output text)
echo "Lambda Function ARN: $LAMBDA_ARN"
rm -rf ./lambda-src function.zip
🧪 Phase 4: Local & Remote CLI Invocations
Before wiring external HTTP APIs, test the function directly via the CLI:
1. Direct Execution via CLI
We pass a mock API Gateway event payload to the Lambda API:
cat <<'EOF' > mock-event.json
{
"rawPath": "/api/v1/health",
"requestContext": {
"http": {
"method": "GET"
}
}
}
EOF
# Invoke Lambda and write response to response.json
aws lambda invoke \
--function-name "$FUNCTION_NAME" \
--payload fileb://mock-event.json \
--cli-binary-format raw-in-base64-out \
--region "$REGION" \
--profile "$PROFILE" \
response.json
echo "Invocation Response:"
cat response.json | jq .
rm -f mock-event.json response.json
Expected output:
{
"statusCode": 200,
"headers": {
"Content-Type": "application/json",
"X-Serverless-Engine": "AWS-Lambda-Graviton"
},
"body": "{\"status\": \"HEALTHY\", \"service\": \"orders-serverless-api\", \"runtime\": \"python3.12-arm64\"}"
}
🌐 Phase 5: Exposing Microservices via HTTP API Gateway v2
To make our serverless function accessible over the public internet, we provision an Amazon API Gateway v2 (HTTP API).
1. Creating the HTTP API
REGION="ap-southeast-1"
PROFILE="cloud-dev-admin"
API_NAME="Orders-Serverless-Gateway"
API_ID=$(aws apigatewayv2 create-api \
--name "$API_NAME" \
--protocol-type "HTTP" \
--cors-configuration '{
"AllowOrigins": ["*"],
"AllowMethods": ["GET", "POST", "OPTIONS"],
"AllowHeaders": ["Content-Type", "Authorization"],
"MaxAge": 3600
}' \
--region "$REGION" \
--profile "$PROFILE" \
--query "ApiId" \
--output text)
echo "HTTP API Gateway v2 Created: $API_ID"
2. Creating the Lambda Proxy Integration
We link the HTTP API Gateway directly to our Lambda function using an AWS_PROXY integration:
INTEGRATION_ID=$(aws apigatewayv2 create-integration \
--api-id "$API_ID" \
--integration-type "AWS_PROXY" \
--integration-uri "$LAMBDA_ARN" \
--payload-format-version "2.0" \
--region "$REGION" \
--profile "$PROFILE" \
--query "IntegrationId" \
--output text)
echo "Integration Created: $INTEGRATION_ID"
3. Creating Default Greedy Route ($default)
We configure a greedy route ($default) so all HTTP traffic (GET /health, POST /orders, etc.) is routed directly to our Python handler:
ROUTE_ID=$(aws apigatewayv2 create-route \
--api-id "$API_ID" \
--route-key '$default' \
--target "integrations/$INTEGRATION_ID" \
--region "$REGION" \
--profile "$PROFILE" \
--query "RouteId" \
--output text)
echo "Route Created: $ROUTE_ID"
4. Deploying the Auto-Deploy $default Stage
In API Gateway v2, the $default stage automatically deploys any route or integration updates immediately:
aws apigatewayv2 create-stage \
--api-id "$API_ID" \
--stage-name '$default' \
--auto-deploy \
--region "$REGION" \
--profile "$PROFILE" >/dev/null 2>&1 || true
API_ENDPOINT=$(aws apigatewayv2 get-api \
--api-id "$API_ID" \
--region "$REGION" \
--profile "$PROFILE" \
--query "ApiEndpoint" \
--output text)
echo "Public Serverless API Endpoint: $API_ENDPOINT"
🛡️ Phase 6: Granting Invocation Permissions & End-to-End Testing
By default, API Gateway is not authorized to invoke your private Lambda function. We must attach a resource-based policy to the Lambda function authorizing the API Gateway source ARN:
1. Authorizing API Gateway to Invoke Lambda
ACCOUNT_ID=$(aws sts get-caller-identity --profile "$PROFILE" --query "Account" --output text)
aws lambda add-permission \
--function-name "$FUNCTION_NAME" \
--statement-id "AllowAPIGatewayInvoke" \
--action "lambda:InvokeFunction" \
--principal "apigateway.amazonaws.com" \
--source-arn "arn:aws:execute-api:${REGION}:${ACCOUNT_ID}:${API_ID}/*" \
--region "$REGION" \
--profile "$PROFILE" >/dev/null 2>&1 || true
echo "API Gateway invocation permissions granted."
2. Testing Live HTTP Invocations with curl
Now send live HTTP requests to your public endpoint:
Test 1: Health Check (GET /api/v1/health)
curl -i -s "${API_ENDPOINT}/api/v1/health"
Test 2: Order Creation (POST /api/v1/orders)
curl -i -s -X POST "${API_ENDPOINT}/api/v1/orders" \
-H "Content-Type: application/json" \
-d '{"item": "Graviton Cloud Server", "quantity": 4}'
Expected HTTP 201 response:
{
"message": "Order processed successfully",
"order_id": "ORD-1726084800000",
"item": "Graviton Cloud Server",
"quantity": 4,
"status": "CONFIRMED"
}
🧹 Phase 7: 1-Click Hardened Lab Teardown
Serverless components cost $0.00 when idle, but cleaning up unwanted endpoints and roles maintains a spotless AWS account:
cat <<'EOF' > destroy-serverless-lab.sh
#!/usr/bin/env bash
set -euo pipefail
REGION="ap-southeast-1"
PROFILE="cloud-dev-admin"
echo "=== 1. Deleting API Gateway v2 ==="
API_ID=$(aws apigatewayv2 get-apis --region "$REGION" --profile "$PROFILE" --query "Items[?Name=='Orders-Serverless-Gateway'].ApiId|[0]" --output text 2>/dev/null || true)
if [ -n "$API_ID" ] && [ "$API_ID" != "None" ]; then
echo "Deleting API Gateway: $API_ID..."
aws apigatewayv2 delete-api --api-id "$API_ID" --region "$REGION" --profile "$PROFILE"
fi
echo "=== 2. Deleting Lambda Function ==="
aws lambda delete-function \
--function-name "orders-serverless-api" \
--region "$REGION" \
--profile "$PROFILE" 2>/dev/null || true
echo "=== 3. Cleaning up IAM Roles & CloudWatch Logs ==="
aws iam detach-role-policy \
--role-name "OrdersLambdaExecutionRole" \
--policy-arn "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" \
--profile "$PROFILE" 2>/dev/null || true
aws iam delete-role \
--role-name "OrdersLambdaExecutionRole" \
--profile "$PROFILE" 2>/dev/null || true
aws logs delete-log-group \
--log-group-name "/aws/lambda/orders-serverless-api" \
--region "$REGION" \
--profile "$PROFILE" 2>/dev/null || true
rm -f destroy-serverless-lab.sh
echo "=== Part 15 Teardown Complete! Zero Serverless Artifacts Remaining. ==="
EOF
chmod +x destroy-serverless-lab.sh
./destroy-serverless-lab.sh
🏆 Key Takeaways & Enterprise Architecture Patterns
- Graviton (ARM64) for Serverless: Always configure
--architectures arm64on AWS Lambda for immediate 20–34% cost and speed advantages over x86. - HTTP API vs. REST API (v1): Choose API Gateway v2 HTTP APIs for microservices. They are 70% cheaper, offer native CORS configuration, and eliminate unnecessary legacy REST overhead.
- Payload Proxy Format 2.0: Modern Lambda integrations use
payload-format-version "2.0"for cleaner JSON headers and simplified event payloads. - Least-Privilege Execution Roles: Never grant Lambda admin permissions. Attach
AWSLambdaBasicExecutionRolefor logging and scoped inline policies for specific DynamoDB or S3 resources. - Cold Start Optimization: Keep function memory appropriately sized (128MB–512MB) and import dependencies outside the handler function for instant subsequent warm invocations.
🔮 What’s Next in Part 16?
Up to this point, we have built infrastructure imperatively using individual CLI commands. But how do enterprise cloud teams codify, review, and reproduce entire multi-tier environments consistently?
In Part 16: Deploying Multi-Tier Stacks with AWS CloudFormation & SAM CLI, we shift from imperative CLI scripts to declarative Infrastructure as Code (IaC). We will write production CloudFormation YAML templates, validate change sets, automate rollbacks, and emulate serverless stacks locally with SAM CLI!