Claude-skill-registry deployment-aws-serverless-setup
Step-by-step guide for setting up AWS serverless deployment with CDK, Lambda, and DynamoDB.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/deployment-aws-serverless-setup" ~/.claude/skills/majiayu000-claude-skill-registry-deployment-aws-serverless-setup && rm -rf "$T"
skills/data/deployment-aws-serverless-setup/SKILL.mdSkill: AWS Serverless Deployment Setup
This skill teaches you how to set up a complete AWS serverless deployment infrastructure for microservices. You'll configure AWS CDK with Go, create Lambda handlers with proper dependency injection, design DynamoDB tables following single-table patterns, integrate EventBridge for domain events, and automate deployment with Makefiles.
AWS Lambda provides a serverless compute platform that scales automatically, charges only for execution time, and eliminates server management overhead. Combined with API Gateway, DynamoDB, and EventBridge, it forms a complete serverless architecture for event-driven microservices following Clean Architecture and DDD patterns.
This setup follows reference architecture patterns, ensuring consistency across all bounded contexts while maintaining independent deployability per service.
Prerequisites
- AWS CLI v2 installed and configured with appropriate credentials
- AWS CDK v2 installed (
)npm install -g aws-cdk - Go 1.25 or later installed
- Understanding of Clean Architecture and bounded contexts
- Node.js 18+ (required for CDK)
- A microservice following directory structure
Overview
In this skill, you will:
- Create the project structure for Lambda deployment
- Implement Go CDK infrastructure code (app, stack, constructs)
- Create Lambda handler entry points for API, Worker, and EventHandler
- Design DynamoDB single-table schema
- Configure EventBridge integration for domain events
- Create Makefile with build, package, and deploy targets
- Deploy and verify the serverless stack
Step 1: Create Deployment Directory Structure
Set up the deployment directory within your microservice to hold CDK code and build artifacts.
Directory Layout
# From your microservice root (e.g., services/go/facilitysvc/) mkdir -p deploy/cdk/constructs mkdir -p dist mkdir -p bin/api mkdir -p bin/worker mkdir -p bin/eventhandler
Final Structure
After setup, your microservice should have:
facilitysvc/ ├── cmd/ │ ├── api/ │ │ └── main.go # API Gateway Lambda │ ├── worker/ │ │ └── main.go # SQS Worker Lambda │ └── eventhandler/ │ └── main.go # EventBridge Lambda ├── core/ │ ├── domain/ │ └── application/ ├── adapters/ │ ├── inbound/ │ └── outbound/ ├── deploy/ │ └── cdk/ │ ├── app.go # CDK app entry point │ ├── stack.go # Service stack definition │ ├── go.mod # CDK dependencies │ └── constructs/ │ ├── lambda.go # Reusable Lambda construct │ ├── api.go # API Gateway construct │ └── table.go # DynamoDB construct ├── dist/ # Build artifacts (zip files) ├── Makefile └── go.mod
This structure separates deployment concerns from application code, following hexagonal principles.
Step 2: Initialize CDK Go Module
Create a separate Go module for CDK infrastructure code to avoid mixing infrastructure dependencies with application code.
// deploy/cdk/go.mod module github.com//services/go/facilitysvc/deploy/cdk go 1.25 require ( github.com/aws/aws-cdk-go/awscdk/v2 v2.180.0 github.com/aws/constructs-go/constructs/v10 v10.4.2 github.com/aws/jsii-runtime-go v1.108.0 )
Initialize with:
cd deploy/cdk go mod init github.com//services/go/facilitysvc/deploy/cdk go get github.com/aws/aws-cdk-go/awscdk/v2@latest go get github.com/aws/constructs-go/constructs/v10@latest go get github.com/aws/jsii-runtime-go@latest go mod tidy
Step 3: Create CDK App Entry Point
The CDK app is the entry point for infrastructure deployment. It creates stacks based on environment variables.
// deploy/cdk/app.go package main import ( "os" "github.com/aws/aws-cdk-go/awscdk/v2" "github.com/aws/jsii-runtime-go" ) func main() { defer jsii.Close() // Create CDK app app := awscdk.NewApp(nil) // Get stage from environment (dev, staging, prod) stage := os.Getenv("STAGE") if stage == "" { stage = "dev" } // Get version for deployment tracking version := os.Getenv("VERSION") if version == "" { version = "latest" } // Get AWS region region := os.Getenv("AWS_REGION") if region == "" { region = "eu-west-1" } // Create service stack // Stack name includes stage for multi-environment support stackName := jsii.String("FacilitySvc-" + stage) NewFacilitySvcStack(app, stackName, &FacilitySvcStackProps{ StackProps: awscdk.StackProps{ Env: &awscdk.Environment{ Region: jsii.String(region), }, }, Stage: stage, Version: version, }) // Synthesize CloudFormation template app.Synth(nil) }
This entry point:
- Reads environment variables to configure the stack
- Creates stage-specific stack names for parallel deployments
- Synthesizes the CDK app into CloudFormation templates
Step 4: Create Service Stack with AWS Resources
Define all AWS resources for the service in a single stack.
// deploy/cdk/stack.go package main import ( "github.com/aws/aws-cdk-go/awscdk/v2" "github.com/aws/aws-cdk-go/awscdk/v2/awsapigatewayv2" "github.com/aws/aws-cdk-go/awscdk/v2/awsdynamodb" "github.com/aws/aws-cdk-go/awscdk/v2/awsevents" "github.com/aws/aws-cdk-go/awscdk/v2/awseventstargets" "github.com/aws/aws-cdk-go/awscdk/v2/awslambda" "github.com/aws/aws-cdk-go/awscdk/v2/awslambdaeventsources" "github.com/aws/aws-cdk-go/awscdk/v2/awssqs" "github.com/aws/constructs-go/constructs/v10" "github.com/aws/jsii-runtime-go" ) // FacilitySvcStackProps defines stack configuration. type FacilitySvcStackProps struct { awscdk.StackProps Stage string Version string } // NewFacilitySvcStack creates the service infrastructure stack. func NewFacilitySvcStack(scope constructs.Construct, id *string, props *FacilitySvcStackProps) awscdk.Stack { stack := awscdk.NewStack(scope, id, &props.StackProps) // DynamoDB Table - single table design table := awsdynamodb.NewTable(stack, jsii.String("FacilitiesTable"), &awsdynamodb.TableProps{ TableName: jsii.String("facilities-" + props.Stage), PartitionKey: &awsdynamodb.Attribute{Name: jsii.String("PK"), Type: awsdynamodb.AttributeType_STRING}, SortKey: &awsdynamodb.Attribute{Name: jsii.String("SK"), Type: awsdynamodb.AttributeType_STRING}, BillingMode: awsdynamodb.BillingMode_PAY_PER_REQUEST, // RETAIN prevents accidental data loss during stack updates RemovalPolicy: awscdk.RemovalPolicy_RETAIN, PointInTimeRecovery: jsii.Bool(true), }) // Add GSI for facility queries table.AddGlobalSecondaryIndex(&awsdynamodb.GlobalSecondaryIndexProps{ IndexName: jsii.String("GSI1"), PartitionKey: &awsdynamodb.Attribute{Name: jsii.String("GSI1PK"), Type: awsdynamodb.AttributeType_STRING}, SortKey: &awsdynamodb.Attribute{Name: jsii.String("GSI1SK"), Type: awsdynamodb.AttributeType_STRING}, }) // API Lambda Handler apiHandler := awslambda.NewFunction(stack, jsii.String("ApiHandler"), &awslambda.FunctionProps{ FunctionName: jsii.String("facilitysvc-api-" + props.Stage), Runtime: awslambda.Runtime_PROVIDED_AL2023(), Handler: jsii.String("bootstrap"), Architecture: awslambda.Architecture_ARM_64(), Code: awslambda.Code_FromAsset(jsii.String("../../dist/api.zip"), nil), MemorySize: jsii.Number(256), Timeout: awscdk.Duration_Seconds(jsii.Number(30)), Environment: &map[string]*string{ "TABLE_NAME": table.TableName(), "STAGE": jsii.String(props.Stage), "VERSION": jsii.String(props.Version), "EVENT_BUS_NAME": jsii.String("default"), }, Tracing: awslambda.Tracing_ACTIVE, }) table.GrantReadWriteData(apiHandler) // SQS Queue for async work queue := awssqs.NewQueue(stack, jsii.String("WorkerQueue"), &awssqs.QueueProps{ QueueName: jsii.String("facilitysvc-worker-" + props.Stage), VisibilityTimeout: awscdk.Duration_Minutes(jsii.Number(5)), DeadLetterQueue: &awssqs.DeadLetterQueue{ Queue: awssqs.NewQueue(stack, jsii.String("WorkerDLQ"), nil), MaxReceiveCount: jsii.Number(3), }, }) // Worker Lambda Handler (processes SQS messages) workerHandler := awslambda.NewFunction(stack, jsii.String("WorkerHandler"), &awslambda.FunctionProps{ FunctionName: jsii.String("facilitysvc-worker-" + props.Stage), Runtime: awslambda.Runtime_PROVIDED_AL2023(), Handler: jsii.String("bootstrap"), Architecture: awslambda.Architecture_ARM_64(), Code: awslambda.Code_FromAsset(jsii.String("../../dist/worker.zip"), nil), MemorySize: jsii.Number(512), Timeout: awscdk.Duration_Minutes(jsii.Number(5)), Environment: &map[string]*string{ "TABLE_NAME": table.TableName(), "STAGE": jsii.String(props.Stage), }, Tracing: awslambda.Tracing_ACTIVE, }) table.GrantReadWriteData(workerHandler) // Connect worker to SQS queue workerHandler.AddEventSource(awslambdaeventsources.NewSqsEventSource(queue, &awslambdaeventsources.SqsEventSourceProps{ BatchSize: jsii.Number(10), })) // EventBridge Event Handler eventHandler := awslambda.NewFunction(stack, jsii.String("EventHandler"), &awslambda.FunctionProps{ FunctionName: jsii.String("facilitysvc-eventhandler-" + props.Stage), Runtime: awslambda.Runtime_PROVIDED_AL2023(), Handler: jsii.String("bootstrap"), Architecture: awslambda.Architecture_ARM_64(), Code: awslambda.Code_FromAsset(jsii.String("../../dist/eventhandler.zip"), nil), MemorySize: jsii.Number(256), Timeout: awscdk.Duration_Seconds(jsii.Number(60)), Environment: &map[string]*string{ "TABLE_NAME": table.TableName(), "STAGE": jsii.String(props.Stage), }, Tracing: awslambda.Tracing_ACTIVE, }) table.GrantReadWriteData(eventHandler) // EventBridge Rule - subscribe to domain events defaultBus := awsevents.EventBus_FromEventBusName(stack, jsii.String("DefaultBus"), jsii.String("default")) rule := awsevents.NewRule(stack, jsii.String("FacilityEventRule"), &awsevents.RuleProps{ EventBus: defaultBus, EventPattern: &awsevents.EventPattern{ Source: jsii.Strings("asset.context"), DetailType: jsii.Strings("AssetStateChanged", "AssetRegistered"), }, }) rule.AddTarget(awseventstargets.NewLambdaFunction(eventHandler, nil)) // API Gateway HTTP API httpApi := awsapigatewayv2.NewHttpApi(stack, jsii.String("HttpApi"), &awsapigatewayv2.HttpApiProps{ ApiName: jsii.String("facilitysvc-" + props.Stage), }) // CloudFormation Outputs awscdk.NewCfnOutput(stack, jsii.String("ApiUrl"), &awscdk.CfnOutputProps{ Value: httpApi.ApiEndpoint(), Description: jsii.String("API Gateway endpoint URL"), }) awscdk.NewCfnOutput(stack, jsii.String("TableName"), &awscdk.CfnOutputProps{ Value: table.TableName(), Description: jsii.String("DynamoDB table name"), }) return stack }
This stack creates:
- DynamoDB table with single-table design (PK/SK)
- API Lambda triggered by API Gateway
- Worker Lambda triggered by SQS
- EventHandler Lambda triggered by EventBridge rules
- Dead-letter queues for reliability
Step 5: Create Lambda Handler Entry Points
Implement Lambda handlers that initialize dependencies once during cold start, then handle requests efficiently.
API Handler
// cmd/api/main.go package main import ( "context" "log" "os" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/eventbridge" "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" "github.com//services/go/facilitysvc/adapters/inbound/http" "github.com//services/go/facilitysvc/adapters/outbound/dynamodbrepo" "github.com//services/go/facilitysvc/adapters/outbound/eventpublisher" "github.com//services/go/facilitysvc/core/application" ) var httpAdapter *httpadapter.HandlerAdapterV2 // init runs once per Lambda cold start - set up dependencies here func init() { ctx := context.Background() // Load AWS SDK config cfg, err := config.LoadDefaultConfig(ctx) if err != nil { log.Fatalf("failed to load AWS config: %v", err) } // Create AWS service clients dynamoClient := dynamodb.NewFromConfig(cfg) eventbridgeClient := eventbridge.NewFromConfig(cfg) // Get environment variables tableName := os.Getenv("TABLE_NAME") eventBusName := os.Getenv("EVENT_BUS_NAME") // Create adapters (hexagonal architecture) repo := dynamodbrepo.NewFacilityRepository(dynamoClient, tableName) publisher := eventpublisher.NewEventBridgePublisher(eventbridgeClient, eventBusName) // Create application service (use cases) appService := application.NewFacilityService(repo, publisher) // Create HTTP router (inbound adapter) router := http.NewRouter(appService) // Wrap router with Lambda API Gateway proxy adapter httpAdapter = httpadapter.NewV2(router) } // handler processes API Gateway requests func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { return httpAdapter.ProxyWithContext(ctx, req) } func main() { lambda.Start(handler) }
Worker Handler
// cmd/worker/main.go package main import ( "context" "encoding/json" "log" "os" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com//services/go/facilitysvc/adapters/outbound/dynamodbrepo" "github.com//services/go/facilitysvc/core/application" ) var appService *application.FacilityService func init() { ctx := context.Background() cfg, err := config.LoadDefaultConfig(ctx) if err != nil { log.Fatalf("failed to load AWS config: %v", err) } dynamoClient := dynamodb.NewFromConfig(cfg) tableName := os.Getenv("TABLE_NAME") repo := dynamodbrepo.NewFacilityRepository(dynamoClient, tableName) appService = application.NewFacilityService(repo, nil) } // handler processes SQS messages with partial batch failure support func handler(ctx context.Context, event events.SQSEvent) (events.SQSEventResponse, error) { var failures []events.SQSBatchItemFailure for _, record := range event.Records { var message application.WorkMessage if err := json.Unmarshal([]byte(record.Body), &message); err != nil { log.Printf("failed to unmarshal message: %v", err) failures = append(failures, events.SQSBatchItemFailure{ItemIdentifier: record.MessageId}) continue } if err := appService.ProcessWorkMessage(ctx, message); err != nil { log.Printf("failed to process message: %v", err) failures = append(failures, events.SQSBatchItemFailure{ItemIdentifier: record.MessageId}) } } // Return partial batch failures - SQS will reprocess only failed messages return events.SQSEventResponse{BatchItemFailures: failures}, nil } func main() { lambda.Start(handler) }
EventHandler Lambda
// cmd/eventhandler/main.go package main import ( "context" "encoding/json" "log" "os" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com//services/go/facilitysvc/adapters/outbound/dynamodbrepo" "github.com//services/go/facilitysvc/core/application" ) var appService *application.FacilityService func init() { ctx := context.Background() cfg, err := config.LoadDefaultConfig(ctx) if err != nil { log.Fatalf("failed to load AWS config: %v", err) } dynamoClient := dynamodb.NewFromConfig(cfg) tableName := os.Getenv("TABLE_NAME") repo := dynamodbrepo.NewFacilityRepository(dynamoClient, tableName) appService = application.NewFacilityService(repo, nil) } // handler processes EventBridge events func handler(ctx context.Context, event events.CloudWatchEvent) error { log.Printf("received event: %s", event.DetailType) var detail map[string]interface{} if err := json.Unmarshal(event.Detail, &detail); err != nil { return err } // Route to appropriate handler based on event type switch event.DetailType { case "AssetStateChanged": return appService.HandleAssetStateChanged(ctx, detail) case "AssetRegistered": return appService.HandleAssetRegistered(ctx, detail) default: log.Printf("unhandled event type: %s", event.DetailType) } return nil } func main() { lambda.Start(handler) }
Step 6: Create Makefile for Build and Deployment
Automate building, packaging, and deploying Lambda functions.
# Makefile SERVICE_NAME := facilitysvc VERSION ?= $(shell git describe --tags --always --dirty) # AWS settings AWS_REGION ?= eu-west-1 STAGE ?= dev # Lambda handlers to build HANDLERS := api worker eventhandler # CDK directory CDK_DIR := deploy/cdk .PHONY: build test package deploy clean ##@ Building # Build all Lambda handlers build: $(HANDLERS) # Build individual handler $(HANDLERS): @echo "Building $@..." @GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build \ -tags lambda.norpc \ -ldflags="-s -w -X main.version=$(VERSION)" \ -o bin/$@/bootstrap \ ./cmd/$@ ##@ Testing test: test-unit test-integration test-unit: go test -v -race -coverprofile=coverage.out ./core/... test-integration: go test -v ./tests/integration/... ##@ Packaging # Package Lambda binaries into zip files package: build @mkdir -p dist @for handler in $(HANDLERS); do \ echo "Packaging $$handler..."; \ cd bin/$$handler && zip -j ../../dist/$$handler.zip bootstrap && cd ../..; \ done ##@ Deployment (CDK) # Bootstrap CDK (run once per account/region) cdk-bootstrap: @cd $(CDK_DIR) && cdk bootstrap aws://$(AWS_ACCOUNT_ID)/$(AWS_REGION) # Synthesize CDK stack (preview CloudFormation) cdk-synth: package @cd $(CDK_DIR) && STAGE=$(STAGE) VERSION=$(VERSION) cdk synth # Deploy to AWS deploy: package ifndef STAGE $(error STAGE is required: make deploy STAGE=dev) endif @echo "Deploying $(SERVICE_NAME) to $(STAGE)..." @cd $(CDK_DIR) && STAGE=$(STAGE) VERSION=$(VERSION) AWS_REGION=$(AWS_REGION) cdk deploy --require-approval never # Show diff before deploying deploy-diff: package @cd $(CDK_DIR) && STAGE=$(STAGE) VERSION=$(VERSION) cdk diff # Destroy stack (careful!) destroy: ifndef STAGE $(error STAGE is required) endif @cd $(CDK_DIR) && STAGE=$(STAGE) cdk destroy --require-approval never ##@ Utilities # Tail Lambda logs logs: ifndef STAGE $(error STAGE is required) endif ifndef HANDLER HANDLER=api endif @aws logs tail /aws/lambda/$(SERVICE_NAME)-$(HANDLER)-$(STAGE) --follow --region $(AWS_REGION) # Invoke API Lambda locally (SAM) invoke-local: build @echo '{"httpMethod":"GET","path":"/facilities"}' | sam local invoke -t $(CDK_DIR)/cdk.out/FacilitySvc-$(STAGE).template.json ApiHandler # Clean build artifacts clean: @rm -rf bin dist $(CDK_DIR)/cdk.out @cd $(CDK_DIR) && rm -rf node_modules .PHONY: help help: @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
Step 7: Deploy and Verify
Deploy your serverless infrastructure to AWS.
First Time Setup
# Bootstrap CDK (once per AWS account/region) export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) make cdk-bootstrap AWS_ACCOUNT_ID=$AWS_ACCOUNT_ID # Deploy to dev make deploy STAGE=dev
Regular Deployment
# Deploy with auto-generated version make deploy STAGE=dev # Deploy with specific version make deploy STAGE=prod VERSION=v1.2.3 # Preview changes before deploying make deploy-diff STAGE=staging
Verify Deployment
# Get stack outputs aws cloudformation describe-stacks \ --stack-name FacilitySvc-dev \ --query 'Stacks[0].Outputs' \ --output table # Test API endpoint API_URL=$(aws cloudformation describe-stacks \ --stack-name FacilitySvc-dev \ --query 'Stacks[0].Outputs[?OutputKey==`ApiUrl`].OutputValue' \ --output text) curl $API_URL/health # Tail Lambda logs make logs STAGE=dev HANDLER=api
Verification Checklist
After completing the AWS serverless setup, verify:
- CDK directory has separate go.mod from application code
- CDK app.go reads STAGE, VERSION, and AWS_REGION from environment
- Stack creates DynamoDB table with PK, SK, and GSI
- Stack includes API, Worker, and EventHandler Lambdas
- Lambda functions use PROVIDED_AL2023 runtime with ARM64 architecture
- All Lambdas have X-Ray tracing enabled
- SQS queue has dead-letter queue configured
- EventBridge rule filters events by source and detail type
- Lambda init() functions set up dependencies (run once per cold start)
- Lambda handlers are thin wrappers delegating to application layer
- Makefile builds for GOOS=linux GOARCH=arm64
- Makefile packages each handler into separate zip files
- Environment variables passed to Lambdas (TABLE_NAME, STAGE)
- CloudFormation outputs include API URL and table name
- CDK bootstrap completed successfully
- Deployment succeeds without errors
- API endpoint returns successful response
- Lambda logs appear in CloudWatch
- DynamoDB table created with correct configuration