Deploy AWS Lambda Functions with Claude Code
The Problem
Deploying AWS Lambda functions involves writing the function code, configuring IAM roles, setting up API Gateway triggers, managing environment variables, handling cold starts, packaging dependencies, and coordinating deployments across multiple functions. The AWS console is tedious, and Infrastructure as Code templates have a steep learning curve.
Quick Start
Ask Claude Code to scaffold a Lambda function with SAM:
Create an AWS Lambda function that:
- Handles POST /api/webhooks/stripe for Stripe webhook processing
- Validates the Stripe signature
- Processes payment events (payment_intent.succeeded, charge.refunded)
- Stores events in DynamoDB
- Uses SAM for deployment
- Includes proper IAM permissions (least privilege)
- Has a local testing setup
What’s Happening
AWS Lambda runs your code without provisioning servers. You upload a function, configure triggers (API Gateway, SQS, S3 events, etc.), and AWS handles scaling, availability, and infrastructure. The Serverless Application Model (SAM) is an extension of CloudFormation that simplifies Lambda deployment.
Claude Code handles the full Lambda development workflow: writing the function, creating the SAM template, configuring permissions, setting up local testing, and preparing the CI/CD pipeline.
Step-by-Step Guide
Step 1: Set up the project structure
Ask Claude Code to create the project:
Set up a SAM project for a webhook processing service with:
- TypeScript Lambda functions
- DynamoDB table for event storage
- API Gateway with custom domain
- Local development with SAM CLI
my-webhook-service/
├── src/
│ ├── handlers/
│ │ ├── stripeWebhook.ts
│ │ └── processEvent.ts
│ ├── lib/
│ │ ├── stripe.ts
│ │ └── dynamodb.ts
│ └── types/
│ └── events.ts
├── tests/
│ ├── unit/
│ │ └── stripeWebhook.test.ts
│ └── events/
│ └── stripe-webhook.json
├── template.yaml
├── samconfig.toml
├── tsconfig.json
├── package.json
└── esbuild.config.ts
Step 2: Write the Lambda function
// src/handlers/stripeWebhook.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import Stripe from 'stripe';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-12-18.acacia',
});
const dynamodb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE_NAME = process.env.EVENTS_TABLE!;
export async function handler(event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> {
// Validate Stripe signature
const signature = event.headers['stripe-signature'];
if (!signature || !event.body) {
return { statusCode: 400, body: JSON.stringify({ error: 'Missing signature or body' }) };
}
let stripeEvent: Stripe.Event;
try {
stripeEvent = stripe.webhooks.constructEvent(
event.body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!,
);
} catch (err) {
console.error('Signature verification failed:', err);
return { statusCode: 400, body: JSON.stringify({ error: 'Invalid signature' }) };
}
// Store event for idempotency and audit
try {
await dynamodb.send(new PutCommand({
TableName: TABLE_NAME,
Item: {
pk: stripeEvent.id,
sk: stripeEvent.type,
data: stripeEvent.data.object,
createdAt: new Date().toISOString(),
processed: false,
},
ConditionExpression: 'attribute_not_exists(pk)', // Idempotency
}));
} catch (err: unknown) {
if ((err as { name: string }).name === 'ConditionalCheckFailedException') {
// Already processed, return success
return { statusCode: 200, body: JSON.stringify({ received: true, duplicate: true }) };
}
throw err;
}
// Process based on event type
switch (stripeEvent.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(stripeEvent.data.object as Stripe.PaymentIntent);
break;
case 'charge.refunded':
await handleRefund(stripeEvent.data.object as Stripe.Charge);
break;
default:
console.log(`Unhandled event type: ${stripeEvent.type}`);
}
return { statusCode: 200, body: JSON.stringify({ received: true }) };
}
async function handlePaymentSuccess(paymentIntent: Stripe.PaymentIntent): Promise<void> {
console.log(`Payment succeeded: ${paymentIntent.id}, amount: ${paymentIntent.amount}`);
// Business logic here
}
async function handleRefund(charge: Stripe.Charge): Promise<void> {
console.log(`Charge refunded: ${charge.id}, amount refunded: ${charge.amount_refunded}`);
// Business logic here
}
Step 3: Create the SAM template
# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Stripe webhook processing service
Globals:
Function:
Timeout: 30
MemorySize: 256
Runtime: nodejs20.x
Architectures:
- arm64 # Graviton2 - cheaper and faster
Environment:
Variables:
EVENTS_TABLE: !Ref EventsTable
Parameters:
Stage:
Type: String
Default: dev
AllowedValues: [dev, staging, prod]
Resources:
StripeWebhookFunction:
Type: AWS::Serverless::Function
Properties:
Handler: dist/handlers/stripeWebhook.handler
Description: Processes Stripe webhook events
Environment:
Variables:
STRIPE_SECRET_KEY: !Sub '{{resolve:secretsmanager:${Stage}/stripe:SecretString:secret_key}}'
STRIPE_WEBHOOK_SECRET: !Sub '{{resolve:secretsmanager:${Stage}/stripe:SecretString:webhook_secret}}'
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref EventsTable
Events:
StripeWebhook:
Type: Api
Properties:
Path: /api/webhooks/stripe
Method: POST
Metadata:
BuildMethod: esbuild
BuildProperties:
Minify: true
Target: es2022
EntryPoints:
- src/handlers/stripeWebhook.ts
EventsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Sub '${Stage}-webhook-events'
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: pk
AttributeType: S
- AttributeName: sk
AttributeType: S
KeySchema:
- AttributeName: pk
KeyType: HASH
- AttributeName: sk
KeyType: RANGE
TimeToLiveSpecification:
AttributeName: ttl
Enabled: true
Outputs:
WebhookUrl:
Description: Stripe webhook URL
Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/api/webhooks/stripe'
Step 4: Optimize for cold starts
Ask Claude Code to reduce cold start latency:
Optimize my Lambda function for minimal cold start time:
- Minimize bundle size with tree-shaking
- Move initialization outside the handler
- Use the AWS SDK v3 (modular imports)
- Consider Lambda SnapStart or provisioned concurrency
// Move SDK initialization outside the handler (reused across invocations)
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
// These are created once during cold start and reused
const client = new DynamoDBClient({});
const dynamodb = DynamoDBDocumentClient.from(client);
// Import only what you need from the SDK (tree-shaking)
// Good: import { PutCommand } from '@aws-sdk/lib-dynamodb'
// Bad: import AWS from 'aws-sdk' (imports everything)
Step 5: Test locally
# Start local API Gateway + Lambda
sam local start-api
# Invoke with a test event
sam local invoke StripeWebhookFunction -e tests/events/stripe-webhook.json
# Run unit tests
npm test
Create test events:
{
"httpMethod": "POST",
"path": "/api/webhooks/stripe",
"headers": {
"stripe-signature": "t=1234567890,v1=test-signature"
},
"body": "{\"id\":\"evt_test\",\"type\":\"payment_intent.succeeded\",\"data\":{\"object\":{\"id\":\"pi_test\",\"amount\":2000}}}"
}
Step 6: Set up deployment pipeline
# .github/workflows/deploy.yml
name: Deploy Lambda
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm test
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-deploy
aws-region: us-east-1
- uses: aws-actions/setup-sam@v2
- run: sam build
- run: sam deploy --no-confirm-changeset --no-fail-on-empty-changeset --parameter-overrides Stage=prod
Step 7: Add monitoring and alerting
Ask Claude Code to add observability:
Add CloudWatch alarms for:
- Function errors > 1% of invocations
- P99 latency > 5 seconds
- Throttling events
- DynamoDB capacity consumption
Include structured logging with correlation IDs.
# Add to template.yaml
ErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub '${Stage}-webhook-errors'
MetricName: Errors
Namespace: AWS/Lambda
Dimensions:
- Name: FunctionName
Value: !Ref StripeWebhookFunction
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 5
ComparisonOperator: GreaterThanThreshold
AlarmActions:
- !Ref AlertTopic
Prevention
Add Lambda development rules to your CLAUDE.md:
## AWS Lambda Rules
- Use ARM64 (Graviton) for all functions
- Keep handlers thin — delegate to service modules
- Initialize SDK clients outside the handler
- Use AWS SDK v3 with modular imports
- Store secrets in Secrets Manager, never in environment variables
- Set function timeout to 2x expected execution time
- Use DynamoDB condition expressions for idempotency
- Every function must have error alarms