| name | lambda |
| description | AWS Lambda and CDK expertise for serverless functions, infrastructure as code, event-driven architectures, cold start optimization, and production patterns. Use when writing Lambda handlers, CDK stacks, NodejsFunction constructs, configuring triggers, optimizing performance, or any serverless compute patterns. Triggers on Lambda, serverless, handler, cold start, event source, API Gateway integration, CDK, infrastructure as code, CloudFormation. |
AWS Lambda & CDK
Expert guidance for AWS Lambda serverless functions and AWS CDK infrastructure as code. Covers handler patterns, event sources, cold start optimization, error handling, CDK constructs, deployment pipelines, and production-ready architectures.
Prerequisites
AWS CLI must be installed and configured. If not set up, prompt the user:
AWS CLI Setup Required:
1. Install: brew install awscli (macOS) or see https://aws.amazon.com/cli/
2. Configure: aws configure
- Enter Access Key ID, Secret Access Key, Region
3. Verify: aws sts get-caller-identity
For CDK specifically:
4. Install CDK: npm install -g aws-cdk
5. Bootstrap account: cdk bootstrap aws://ACCOUNT-ID/REGION
Core Philosophy
Lambda is not a server. It's an event-driven compute service with different constraints. Embrace statelessness, design for cold starts, and let AWS handle scaling.
Minimize cold starts. Cold starts are the #1 performance concern. Keep bundles small, initialize outside the handler, and choose the right memory/architecture.
Cost follows invocations. You pay per request and GB-seconds. Optimize for execution time, not uptime. Right-size memory (often higher memory = faster = cheaper).
Function Configuration
Runtime Selection
Recommended for TypeScript/JavaScript:
- nodejs20.x (LTS, best performance)
- nodejs22.x (latest features)
Avoid:
- nodejs18.x (approaching EOL)
- Custom runtimes (complexity, cold starts)
Memory & Architecture
const MEMORY_GUIDELINES = {
light: 256,
standard: 512,
heavy: 1024,
intensive: 1769,
max: 3008,
};
const ARCHITECTURE = 'arm64';
Timeout Guidelines
API Gateway integration: 29 seconds max (API Gateway limit)
Direct invocation: Up to 15 minutes
Event processing: Match SQS visibility timeout
Rule: Set timeout to 2-3x expected execution time
Never use max (15 min) unless truly needed
Essential Configuration
const functionConfig = {
runtime: 'nodejs20.x',
architecture: 'arm64',
memorySize: 512,
timeout: 30,
environment: {
NODE_ENV: 'production',
LOG_LEVEL: 'info',
},
reservedConcurrency: 100,
provisionedConcurrency: 5,
};
Handler Patterns
Basic Handler Structure
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda';
const dbPool = createDbPool();
const config = loadConfig();
export const handler: APIGatewayProxyHandlerV2 = async (event, context) => {
context.callbackWaitsForEmptyEventLoop = false;
try {
const result = await processRequest(event);
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result),
};
} catch (error) {
console.error('Handler error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' }),
};
}
};
Typed Event Handlers
import type {
APIGatewayProxyHandlerV2,
SQSHandler,
S3Handler,
EventBridgeHandler,
ScheduledHandler,
} from 'aws-lambda';
export const apiHandler: APIGatewayProxyHandlerV2 = async (event) => {
const { pathParameters, queryStringParameters, body } = event;
};
export const sqsHandler: SQSHandler = async (event) => {
for (const record of event.Records) {
const body = JSON.parse(record.body);
await processMessage(body);
}
};
export const s3Handler: S3Handler = async (event) => {
for (const record of event.Records) {
const bucket = record.s3.bucket.name;
const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '));
await processObject(bucket, key);
}
};
export const eventBridgeHandler: EventBridgeHandler<
'custom.event',
{ orderId: string }
> = async (event) => {
const { orderId } = event.detail;
await processOrder(orderId);
};
export const scheduledHandler: ScheduledHandler = async (event) => {
console.log('Scheduled execution:', event.time);
await runMaintenanceTask();
};
Response Patterns
const success = (data: unknown) => ({
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'max-age=300',
},
body: JSON.stringify(data),
});
const error = (statusCode: number, message: string) => ({
statusCode,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: message }),
});
const redirect = (location: string) => ({
statusCode: 302,
headers: { Location: location },
body: '',
});
const noContent = () => ({
statusCode: 204,
body: '',
});
Cold Start Optimization
Understanding Cold Starts
Cold start phases:
1. Container init (~100-500ms) - AWS provisions container
2. Runtime init (~50-200ms) - Node.js starts
3. Handler init (variable) - Your code outside handler runs
4. Handler execution - Your handler runs
You control: Handler init + Handler execution
AWS controls: Container init + Runtime init
Minimization Strategies
const s3 = new S3Client({ region: 'us-east-1' });
const dbPool = createPool(process.env.DATABASE_URL);
export const handler = async (event) => {
await s3.send(new GetObjectCommand({ }));
};
export const handler = async (event) => {
const s3 = new S3Client({ region: 'us-east-1' });
};
let heavyClient: HeavyClient | null = null;
function getHeavyClient() {
if (!heavyClient) {
heavyClient = new HeavyClient();
}
return heavyClient;
}
export const handler = async (event) => {
if (event.needsHeavyProcessing) {
const client = getHeavyClient();
}
};
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import AWS from 'aws-sdk';
Bundle Size Optimization
import { build } from 'esbuild';
await build({
entryPoints: ['src/handler.ts'],
bundle: true,
minify: true,
sourcemap: true,
platform: 'node',
target: 'node20',
outfile: 'dist/handler.js',
external: [
'@aws-sdk/*',
],
treeShaking: true,
});
Provisioned Concurrency
Error Handling & Retries
Structured Error Handling
class ValidationError extends Error {
constructor(message: string, public field: string) {
super(message);
this.name = 'ValidationError';
}
}
class NotFoundError extends Error {
constructor(public resource: string, public id: string) {
super(`${resource} not found: ${id}`);
this.name = 'NotFoundError';
}
}
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
try {
const result = await processRequest(event);
return success(result);
} catch (err) {
if (err instanceof ValidationError) {
return error(400, err.message);
}
if (err instanceof NotFoundError) {
return error(404, err.message);
}
console.error('Unexpected error:', err);
return error(500, 'Internal server error');
}
};
Retry Behavior by Event Source
Event Source Retry Behavior
─────────────────────────────────────────────
API Gateway No retries (synchronous)
SQS Retries until visibility timeout
Then DLQ after maxReceiveCount
SNS 3 retries, exponential backoff
EventBridge 185 retries over 24 hours
S3 Retries on failure
DynamoDB Streams Retries until data expires (24h)
Kinesis Retries until data expires (7d default)
Dead Letter Queues
const functionConfig = {
deadLetterQueue: {
targetArn: 'arn:aws:sqs:us-east-1:123456789:my-dlq',
},
onFailure: {
destination: 'arn:aws:sqs:us-east-1:123456789:my-dlq',
},
onSuccess: {
destination: 'arn:aws:events:us-east-1:123456789:event-bus/default',
},
};
Idempotency
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
const dynamodb = new DynamoDBClient({});
async function ensureIdempotent(
idempotencyKey: string,
operation: () => Promise<void>
) {
try {
await dynamodb.send(new PutItemCommand({
TableName: 'idempotency',
Item: {
pk: { S: idempotencyKey },
ttl: { N: String(Math.floor(Date.now() / 1000) + 86400) },
status: { S: 'processing' },
},
ConditionExpression: 'attribute_not_exists(pk)',
}));
await operation();
await dynamodb.send(new PutItemCommand({
TableName: 'idempotency',
Item: {
pk: { S: idempotencyKey },
ttl: { N: String(Math.floor(Date.now() / 1000) + 86400) },
status: { S: 'completed' },
},
}));
} catch (err) {
if (err.name === 'ConditionalCheckFailedException') {
console.log('Duplicate request, skipping:', idempotencyKey);
return;
}
throw err;
}
}
export const sqsHandler: SQSHandler = async (event) => {
for (const record of event.Records) {
await ensureIdempotent(record.messageId, async () => {
await processMessage(JSON.parse(record.body));
});
}
};
Logging & Monitoring
Structured Logging
function log(level: string, message: string, data?: Record<string, unknown>) {
console.log(JSON.stringify({
level,
message,
timestamp: new Date().toISOString(),
requestId: process.env.AWS_REQUEST_ID,
...data,
}));
}
log('info', 'Processing order', { orderId: '123', items: 5 });
log('error', 'Payment failed', { orderId: '123', error: err.message });
Key Metrics to Monitor
Metric Alert Threshold Why
────────────────────────────────────────────────────
Errors > 1% of invocations Failures
Duration > 80% of timeout Timeout risk
ConcurrentExecutions > 80% of limit Throttling risk
Throttles > 0 Capacity issue
IteratorAge (streams) > 60 seconds Processing lag
X-Ray Tracing
import { captureAWSv3Client } from 'aws-xray-sdk-core';
import { S3Client } from '@aws-sdk/client-s3';
const s3 = captureAWSv3Client(new S3Client({}));
import AWSXRay from 'aws-xray-sdk-core';
export const handler = async (event) => {
const segment = AWSXRay.getSegment();
const subsegment = segment?.addNewSubsegment('processOrder');
try {
const result = await processOrder(event);
subsegment?.close();
return result;
} catch (err) {
subsegment?.addError(err);
subsegment?.close();
throw err;
}
};
Event Source Patterns
API Gateway (HTTP API)
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda';
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
const { id } = event.pathParameters || {};
const { page = '1' } = event.queryStringParameters || {};
const body = event.body ? JSON.parse(event.body) : null;
const authHeader = event.headers['authorization'];
const { requestId, http } = event.requestContext;
console.log(`${http.method} ${http.path} - ${requestId}`);
return {
statusCode: 200,
body: JSON.stringify({ id, page }),
};
};
SQS with Batch Processing
import type { SQSHandler, SQSBatchResponse } from 'aws-lambda';
export const handler: SQSHandler = async (event): Promise<SQSBatchResponse> => {
const batchItemFailures: { itemIdentifier: string }[] = [];
await Promise.all(
event.Records.map(async (record) => {
try {
await processMessage(JSON.parse(record.body));
} catch (err) {
console.error('Failed to process:', record.messageId, err);
batchItemFailures.push({ itemIdentifier: record.messageId });
}
})
);
return { batchItemFailures };
};
EventBridge with Filtering
const eventPattern = {
source: ['my.application'],
'detail-type': ['OrderCreated'],
detail: {
status: ['paid'],
amount: [{ numeric: ['>=', 100] }],
},
};
import type { EventBridgeHandler } from 'aws-lambda';
interface OrderEvent {
orderId: string;
status: string;
amount: number;
}
export const handler: EventBridgeHandler<'OrderCreated', OrderEvent> = async (
event
) => {
const { orderId, amount } = event.detail;
await notifyHighValueOrder(orderId, amount);
};
S3 Event Processing
For mounting S3 buckets as a filesystem inside Lambda (S3 Files), see the s3 skill.
import type { S3Handler } from 'aws-lambda';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({});
export const handler: S3Handler = async (event) => {
for (const record of event.Records) {
if (!record.eventName.startsWith('ObjectCreated:')) continue;
const bucket = record.s3.bucket.name;
const key = decodeURIComponent(
record.s3.object.key.replace(/\+/g, ' ')
);
if (!key.startsWith('uploads/')) continue;
const { Body } = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
const content = await Body?.transformToString();
await processUpload(key, content);
}
};
Scheduled Tasks (Cron)
import type { ScheduledHandler } from 'aws-lambda';
export const handler: ScheduledHandler = async (event) => {
console.log('Scheduled execution:', event.time);
await cleanupExpiredSessions();
await sendDailyDigests();
await refreshCaches();
};
Security Best Practices
IAM Least Privilege
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-bucket/uploads/*"
},
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:us-east-1:*:table/my-table"
},
{
"Effect": "Allow",
"Action": "sqs:SendMessage",
"Resource": "arn:aws:sqs:us-east-1:*:my-queue"
}
]
}
Secrets Management
import {
SecretsManagerClient,
GetSecretValueCommand,
} from '@aws-sdk/client-secrets-manager';
const secrets = new SecretsManagerClient({});
let cachedSecrets: Record<string, string> | null = null;
async function getSecrets(): Promise<Record<string, string>> {
if (cachedSecrets) return cachedSecrets;
const { SecretString } = await secrets.send(
new GetSecretValueCommand({ SecretId: 'my-app/production' })
);
cachedSecrets = JSON.parse(SecretString!);
return cachedSecrets;
}
export const handler = async (event) => {
const { DATABASE_URL, API_KEY } = await getSecrets();
};
VPC Configuration
When to use VPC:
✅ Accessing RDS/ElastiCache in private subnet
✅ Compliance requirements (network isolation)
✅ Connecting to on-prem resources via Direct Connect
When to avoid VPC:
❌ Only accessing public AWS services (S3, DynamoDB, SQS)
❌ Latency-sensitive functions (VPC adds cold start time)
❌ Internet access needed (requires NAT Gateway, adds cost)
If using VPC:
- Use VPC endpoints for AWS services (no NAT needed)
- Place Lambda in private subnets
- NAT Gateway only if internet access truly required
Input Validation
import { z } from 'zod';
const OrderSchema = z.object({
customerId: z.string().uuid(),
items: z.array(z.object({
productId: z.string(),
quantity: z.number().int().positive(),
})).min(1),
shippingAddress: z.object({
line1: z.string().max(100),
city: z.string().max(50),
country: z.string().length(2),
}),
});
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
const body = JSON.parse(event.body || '{}');
const result = OrderSchema.safeParse(body);
if (!result.success) {
return {
statusCode: 400,
body: JSON.stringify({
error: 'Validation failed',
details: result.error.flatten(),
}),
};
}
const order = result.data;
};
Cost Optimization
Pricing Model
Lambda pricing (us-east-1):
- Requests: $0.20 per 1M requests
- Duration: $0.0000166667 per GB-second (x86)
- Duration: $0.0000133334 per GB-second (ARM, 20% cheaper)
Free tier (monthly):
- 1M requests
- 400,000 GB-seconds
Cost Estimation
function estimateLambdaCost(params: {
invocationsPerMonth: number;
avgDurationMs: number;
memoryMB: number;
architecture?: 'x86' | 'arm64';
}) {
const { invocationsPerMonth, avgDurationMs, memoryMB, architecture = 'arm64' } = params;
const requestCost = (invocationsPerMonth / 1_000_000) * 0.20;
const gbSeconds = (invocationsPerMonth * avgDurationMs / 1000) * (memoryMB / 1024);
const durationRate = architecture === 'arm64' ? 0.0000133334 : 0.0000166667;
const durationCost = gbSeconds * durationRate;
const freeRequests = Math.min(invocationsPerMonth, 1_000_000);
const freeGbSeconds = Math.min(gbSeconds, 400_000);
return {
requests: requestCost,
duration: durationCost,
freeRequestSavings: (freeRequests / 1_000_000) * 0.20,
freeDurationSavings: freeGbSeconds * durationRate,
total: Math.max(0, requestCost + durationCost - (freeRequests / 1_000_000) * 0.20 - freeGbSeconds * durationRate),
};
}
Optimization Checklist
[ ] Use ARM architecture (20% cheaper)
[ ] Right-size memory (test with AWS Lambda Power Tuning)
[ ] Minimize bundle size (faster cold starts, less duration)
[ ] Use Provisioned Concurrency only where needed
[ ] Batch process where possible (fewer invocations)
[ ] Use SQS batch size > 1 for queue processing
[ ] Set appropriate timeouts (don't pay for idle time)
[ ] Use Savings Plans for predictable workloads
[ ] Delete unused functions
[ ] Monitor with Cost Explorer by function tag
Local Development
SAM Local
brew install aws-sam-cli
sam local invoke MyFunction -e event.json
sam local start-api
sam local invoke --debug-port 5858
Testing
import { mockClient } from 'aws-sdk-client-mock';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { handler } from './handler';
const s3Mock = mockClient(S3Client);
beforeEach(() => {
s3Mock.reset();
});
test('processes S3 object', async () => {
s3Mock.on(GetObjectCommand).resolves({
Body: { transformToString: () => Promise.resolve('file content') },
});
const event = {
Records: [{
s3: {
bucket: { name: 'my-bucket' },
object: { key: 'uploads/test.txt' },
},
eventName: 'ObjectCreated:Put',
}],
};
await handler(event as any, {} as any, () => {});
expect(s3Mock.calls()).toHaveLength(1);
});
Troubleshooting
Common Errors
Task timed out after X seconds
- Increase timeout setting
- Check for blocking operations
- Verify VPC/NAT configuration if using VPC
- Check downstream service latency
Module not found
- Bundle dependencies correctly
- Check external modules configuration
- Verify layer contents
AccessDenied
- Check IAM role permissions
- Verify resource ARNs
- Check resource-based policies
ECONNREFUSED (VPC)
- Security group missing outbound rules
- No route to NAT Gateway
- VPC endpoint not configured
Debugging Tips
export const handler = async (event, context) => {
console.log('Event:', JSON.stringify(event, null, 2));
console.log('Context:', JSON.stringify({
functionName: context.functionName,
functionVersion: context.functionVersion,
memoryLimitInMB: context.memoryLimitInMB,
remainingTimeMs: context.getRemainingTimeInMillis(),
}));
};
aws logs tail /aws/lambda/my-function --follow
aws logs filter-log-events \
--log-group-name /aws/lambda/my-function \
--filter-pattern "ERROR"
Quick Reference (Lambda)
Handler Types:
- APIGatewayProxyHandlerV2 → HTTP API
- APIGatewayProxyHandler → REST API
- SQSHandler → SQS Queue
- S3Handler → S3 Events
- EventBridgeHandler → EventBridge
- ScheduledHandler → CloudWatch Events
- DynamoDBStreamHandler → DynamoDB Streams
- KinesisStreamHandler → Kinesis
Limits:
- Timeout: 15 minutes max
- Memory: 128MB - 10,240MB
- Package: 50MB zipped, 250MB unzipped
- /tmp: 512MB - 10,240MB
- Concurrent executions: 1,000 default (adjustable)
- Burst concurrency: 500-3000 (region-dependent)
Response Sizes:
- Synchronous: 6MB
- Asynchronous: 256KB payload
Cold Start Factors:
- Package size (biggest impact)
- VPC configuration
- Memory allocation
- Runtime
- Provisioned concurrency (eliminates cold starts)
AWS CDK
Infrastructure as code for Lambda and serverless architectures using AWS Cloud Development Kit v2.
CDK Philosophy
Infrastructure IS code. CDK uses TypeScript to define cloud resources with full IDE support, type safety, and the ability to share/reuse patterns. No YAML/JSON templates.
Constructs are the building blocks. L1 (raw CloudFormation), L2 (opinionated defaults), L3 (patterns combining multiple resources). Prefer L2/L3 for productivity.
Synthesize, don't mutate. CDK synthesizes CloudFormation templates. All decisions happen at synthesis time, not deploy time. Use TypeScript conditionals, not CloudFormation conditions.
Project Structure
Recommended Layout
my-lambda-app/
├── bin/
│ └── app.ts # CDK app entry point
├── lib/
│ ├── stacks/
│ │ ├── api-stack.ts # API Gateway + Lambda stack
│ │ └── database-stack.ts # DynamoDB stack
│ ├── constructs/
│ │ ├── api-handler.ts # Custom L3 construct
│ │ └── queue-processor.ts # Custom L3 construct
│ └── lambdas/ # Lambda handler code
│ ├── api/
│ │ └── handler.ts
│ └── processor/
│ └── handler.ts
├── test/
│ └── *.test.ts # Infrastructure tests
├── cdk.json # CDK configuration
├── package.json # Single file for CDK + runtime deps
└── tsconfig.json
Key Principles
NodejsFunction Construct
The preferred way to define Lambda functions in CDK. Auto-transpiles TypeScript with esbuild.
Basic Usage
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Runtime, Architecture } from 'aws-cdk-lib/aws-lambda';
const apiHandler = new NodejsFunction(this, 'api', {
runtime: Runtime.NODEJS_20_X,
architecture: Architecture.ARM_64,
memorySize: 512,
timeout: Duration.seconds(30),
});
const processor = new NodejsFunction(this, 'processor', {
entry: path.join(__dirname, '../lambdas/processor/handler.ts'),
handler: 'main',
runtime: Runtime.NODEJS_20_X,
architecture: Architecture.ARM_64,
});
Bundling Configuration
new NodejsFunction(this, 'optimized-handler', {
entry: path.join(__dirname, '../lambdas/api/handler.ts'),
runtime: Runtime.NODEJS_20_X,
architecture: Architecture.ARM_64,
bundling: {
minify: true,
sourceMap: true,
sourceMapMode: SourceMapMode.INLINE,
target: 'es2022',
externalModules: ['@aws-sdk/*'],
nodeModules: ['sharp', 'bcrypt'],
forceDockerBundling: true,
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
},
esbuildArgs: {
'--tree-shaking': 'true',
},
},
});
Environment Variables & Secrets
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
const apiKey = Secret.fromSecretNameV2(this, 'ApiKey', 'my-api-key');
const configParam = StringParameter.fromStringParameterName(
this, 'Config', '/my-app/config'
);
const handler = new NodejsFunction(this, 'handler', {
entry: path.join(__dirname, '../lambdas/handler.ts'),
environment: {
LOG_LEVEL: 'info',
TABLE_NAME: table.tableName,
API_KEY_SECRET_ARN: apiKey.secretArn,
CONFIG_PARAM_NAME: configParam.parameterName,
},
});
apiKey.grantRead(handler);
configParam.grantRead(handler);
Construct Patterns
L2 Constructs with Grants (Preferred)
import { Table, AttributeType } from 'aws-cdk-lib/aws-dynamodb';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { Queue } from 'aws-cdk-lib/aws-sqs';
const table = new Table(this, 'Orders', {
partitionKey: { name: 'pk', type: AttributeType.STRING },
sortKey: { name: 'sk', type: AttributeType.STRING },
billingMode: BillingMode.PAY_PER_REQUEST,
removalPolicy: RemovalPolicy.RETAIN,
});
const bucket = new Bucket(this, 'Uploads', {
encryption: BucketEncryption.S3_MANAGED,
removalPolicy: RemovalPolicy.RETAIN,
});
const queue = new Queue(this, 'ProcessingQueue', {
visibilityTimeout: Duration.seconds(300),
deadLetterQueue: {
queue: new Queue(this, 'DLQ'),
maxReceiveCount: 3,
},
});
table.grantReadWriteData(handler);
bucket.grantRead(handler);
queue.grantSendMessages(handler);
handler.addEnvironment('TABLE_NAME', table.tableName);
handler.addEnvironment('BUCKET_NAME', bucket.bucketName);
handler.addEnvironment('QUEUE_URL', queue.queueUrl);
Custom L3 Construct (Reusable Pattern)
import { Construct } from 'constructs';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Queue } from 'aws-cdk-lib/aws-sqs';
import { SqsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources';
export interface QueueProcessorProps {
readonly entry: string;
readonly batchSize?: number;
readonly maxConcurrency?: number;
}
export class QueueProcessor extends Construct {
public readonly queue: Queue;
public readonly handler: NodejsFunction;
public readonly dlq: Queue;
constructor(scope: Construct, id: string, props: QueueProcessorProps) {
super(scope, id);
this.dlq = new Queue(this, 'DLQ', {
retentionPeriod: Duration.days(14),
});
this.queue = new Queue(this, 'Queue', {
visibilityTimeout: Duration.seconds(300),
deadLetterQueue: {
queue: this.dlq,
maxReceiveCount: 3,
},
});
this.handler = new NodejsFunction(this, 'Handler', {
entry: props.entry,
runtime: Runtime.NODEJS_20_X,
architecture: Architecture.ARM_64,
memorySize: 512,
timeout: Duration.seconds(300),
reservedConcurrentExecutions: props.maxConcurrency,
});
this.handler.addEventSource(new SqsEventSource(this.queue, {
batchSize: props.batchSize ?? 10,
reportBatchItemFailures: true,
}));
}
}
const orderProcessor = new QueueProcessor(this, 'OrderProcessor', {
entry: path.join(__dirname, '../lambdas/process-order/handler.ts'),
batchSize: 5,
maxConcurrency: 10,
});
API Gateway Integration
HTTP API (Recommended)
import { HttpApi, HttpMethod } from 'aws-cdk-lib/aws-apigatewayv2';
import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations';
const api = new HttpApi(this, 'Api', {
corsPreflight: {
allowOrigins: ['https://myapp.com'],
allowMethods: [CorsHttpMethod.GET, CorsHttpMethod.POST],
allowHeaders: ['Content-Type', 'Authorization'],
},
});
const getOrdersIntegration = new HttpLambdaIntegration(
'GetOrders',
getOrdersHandler
);
const createOrderIntegration = new HttpLambdaIntegration(
'CreateOrder',
createOrderHandler
);
api.addRoutes({
path: '/orders',
methods: [HttpMethod.GET],
integration: getOrdersIntegration,
});
api.addRoutes({
path: '/orders',
methods: [HttpMethod.POST],
integration: createOrderIntegration,
});
api.addRoutes({
path: '/orders/{id}',
methods: [HttpMethod.GET],
integration: getOrderIntegration,
});
REST API with Lambda Proxy
import { RestApi, LambdaIntegration } from 'aws-cdk-lib/aws-apigateway';
const api = new RestApi(this, 'Api', {
deployOptions: {
stageName: 'prod',
throttlingBurstLimit: 100,
throttlingRateLimit: 50,
},
});
const orders = api.root.addResource('orders');
orders.addMethod('GET', new LambdaIntegration(listOrdersHandler));
orders.addMethod('POST', new LambdaIntegration(createOrderHandler));
const order = orders.addResource('{id}');
order.addMethod('GET', new LambdaIntegration(getOrderHandler));
Event-Driven Patterns
S3 Triggers
import { S3EventSourceV2 } from 'aws-cdk-lib/aws-lambda-event-sources';
const bucket = new Bucket(this, 'Uploads');
const processor = new NodejsFunction(this, 'ImageProcessor', {
entry: path.join(__dirname, '../lambdas/image-processor/handler.ts'),
});
processor.addEventSource(new S3EventSourceV2(bucket, {
events: [EventType.OBJECT_CREATED],
filters: [{ prefix: 'uploads/', suffix: '.jpg' }],
}));
EventBridge Rules
import { Rule, Schedule } from 'aws-cdk-lib/aws-events';
import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets';
new Rule(this, 'DailyCleanup', {
schedule: Schedule.cron({ hour: '2', minute: '0' }),
targets: [new LambdaFunction(cleanupHandler)],
});
new Rule(this, 'OrderCreated', {
eventPattern: {
source: ['my.application'],
detailType: ['OrderCreated'],
detail: {
status: ['paid'],
},
},
targets: [new LambdaFunction(orderHandler)],
});
DynamoDB Streams
import { DynamoEventSource } from 'aws-cdk-lib/aws-lambda-event-sources';
import { StartingPosition } from 'aws-cdk-lib/aws-lambda';
const table = new Table(this, 'Orders', {
partitionKey: { name: 'pk', type: AttributeType.STRING },
stream: StreamViewType.NEW_AND_OLD_IMAGES,
});
streamProcessor.addEventSource(new DynamoEventSource(table, {
startingPosition: StartingPosition.TRIM_HORIZON,
batchSize: 100,
bisectBatchOnError: true,
retryAttempts: 3,
}));
Stack Organization
Separate Stateful and Stateless
const app = new App();
const dataStack = new DataStack(app, 'DataStack', {
env: { account: '123456789', region: 'us-east-1' },
terminationProtection: true,
});
const apiStack = new ApiStack(app, 'ApiStack', {
env: { account: '123456789', region: 'us-east-1' },
table: dataStack.ordersTable,
bucket: dataStack.uploadsBucket,
});
Environment Configuration
interface EnvironmentConfig {
readonly account: string;
readonly region: string;
readonly domainName?: string;
readonly logRetention: RetentionDays;
readonly enableAlarms: boolean;
}
const environments: Record<string, EnvironmentConfig> = {
dev: {
account: '111111111111',
region: 'us-east-1',
logRetention: RetentionDays.ONE_WEEK,
enableAlarms: false,
},
prod: {
account: '222222222222',
region: 'us-east-1',
domainName: 'api.myapp.com',
logRetention: RetentionDays.ONE_YEAR,
enableAlarms: true,
},
};
const stage = app.node.tryGetContext('stage') || 'dev';
const config = environments[stage];
new ApiStack(app, `Api-${stage}`, {
env: { account: config.account, region: config.region },
...config,
});
Security & Compliance
cdk-nag for Automated Checks
import { Aspects } from 'aws-cdk-lib';
import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag';
const app = new App();
const stack = new MyStack(app, 'MyStack');
Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));
NagSuppressions.addStackSuppressions(stack, [
{
id: 'AwsSolutions-IAM4',
reason: 'Using managed policy for CloudWatch Logs is acceptable',
},
]);
NagSuppressions.addResourceSuppressions(
myLambda,
[{ id: 'AwsSolutions-L1', reason: 'Node 18 required for SDK compatibility' }],
true
);
Custom Aspects for Compliance
import { IAspect, Annotations } from 'aws-cdk-lib';
import { CfnFunction } from 'aws-cdk-lib/aws-lambda';
class LambdaSecurityAspect implements IAspect {
visit(node: IConstruct): void {
if (node instanceof CfnFunction) {
if (node.architectures?.[0] !== 'arm64') {
Annotations.of(node).addWarning(
'Lambda should use ARM64 for cost optimization'
);
}
const memory = node.memorySize || 128;
if (memory > 3008) {
Annotations.of(node).addError(
'Lambda memory exceeds approved limit of 3008MB'
);
}
const timeout = node.timeout || 3;
if (timeout > 300) {
Annotations.of(node).addWarning(
'Lambda timeout exceeds 5 minutes - consider Step Functions'
);
}
}
}
}
Aspects.of(stack).add(new LambdaSecurityAspect());
Least Privilege IAM
table.grantReadWriteData(handler);
bucket.grantRead(handler);
queue.grantSendMessages(handler);
handler.addToRolePolicy(new PolicyStatement({
actions: ['dynamodb:*'],
resources: ['*'],
}));
handler.addToRolePolicy(new PolicyStatement({
actions: ['ses:SendEmail'],
resources: [`arn:aws:ses:${region}:${account}:identity/myapp.com`],
}));
Testing CDK Applications
Snapshot Tests
import { Template } from 'aws-cdk-lib/assertions';
import { App } from 'aws-cdk-lib';
test('stack matches snapshot', () => {
const app = new App();
const stack = new MyStack(app, 'TestStack');
const template = Template.fromStack(stack);
expect(template.toJSON()).toMatchSnapshot();
});
Fine-Grained Assertions
import { Template, Match } from 'aws-cdk-lib/assertions';
test('creates Lambda with correct configuration', () => {
const app = new App();
const stack = new MyStack(app, 'TestStack');
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::Lambda::Function', {
Runtime: 'nodejs20.x',
Architectures: ['arm64'],
MemorySize: 512,
Timeout: 30,
});
template.hasResourceProperties('AWS::IAM::Policy', {
PolicyDocument: {
Statement: Match.arrayWith([
Match.objectLike({
Action: Match.arrayWith(['dynamodb:GetItem', 'dynamodb:PutItem']),
}),
]),
},
});
});
Logical ID Stability Tests
test('stateful resource logical IDs are stable', () => {
const app = new App();
const stack = new DataStack(app, 'DataStack');
const template = Template.fromStack(stack);
expect(template.findResources('AWS::DynamoDB::Table')).toHaveProperty(
'OrdersTable8A7D7F12'
);
expect(template.findResources('AWS::S3::Bucket')).toHaveProperty(
'UploadsBucket4F81B48C'
);
});
CDK Pipelines (CI/CD)
Basic Pipeline
import { CodePipeline, CodePipelineSource, ShellStep } from 'aws-cdk-lib/pipelines';
const pipeline = new CodePipeline(this, 'Pipeline', {
pipelineName: 'MyAppPipeline',
synth: new ShellStep('Synth', {
input: CodePipelineSource.gitHub('owner/repo', 'main'),
commands: [
'npm ci',
'npm run build',
'npm run test',
'npx cdk synth',
],
}),
crossAccountKeys: true,
});
pipeline.addStage(new DeployStage(this, 'Dev', {
env: { account: '111111111111', region: 'us-east-1' },
}));
pipeline.addStage(new DeployStage(this, 'Prod', {
env: { account: '222222222222', region: 'us-east-1' },
}), {
pre: [
new ManualApprovalStep('PromoteToProd'),
],
});
Cross-Account Bootstrap
cdk bootstrap aws://PIPELINE_ACCOUNT/us-east-1
cdk bootstrap aws://DEV_ACCOUNT/us-east-1 \
--trust PIPELINE_ACCOUNT \
--cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess
cdk bootstrap aws://PROD_ACCOUNT/us-east-1 \
--trust PIPELINE_ACCOUNT \
--cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess
CDK Best Practices Summary
DO
✅ Use NodejsFunction for TypeScript Lambda handlers
✅ Co-locate infrastructure and runtime code
✅ Use grant methods for IAM (grantRead, grantWrite, etc.)
✅ Let CDK generate resource names (no hardcoded names)
✅ Separate stateful and stateless resources into different stacks
✅ Enable termination protection on stateful stacks
✅ Commit cdk.context.json to version control
✅ Use cdk-nag for security/compliance checks
✅ Write tests that assert logical ID stability for stateful resources
✅ Configure via properties, not environment variables
✅ Make decisions at synthesis time (TypeScript conditionals)
✅ Model with constructs, deploy with stacks
DON'T
❌ Use hardcoded physical names (prevents multiple deployments)
❌ Look up environment variables inside constructs
❌ Change logical IDs of stateful resources (causes replacement)
❌ Rely solely on wrapper constructs for compliance
❌ Use CloudFormation conditions (use TypeScript instead)
❌ Mix stateful and stateless resources in same stack
❌ Skip cdk-nag or similar security scanning
❌ Deploy without testing template changes
❌ Use overly broad IAM permissions
❌ Forget removal policies on stateful resources
Anti-Patterns
export class MyConstruct extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
const bucketName = process.env.BUCKET_NAME;
}
}
export interface MyConstructProps {
readonly bucketName: string;
}
export class MyConstruct extends Construct {
constructor(scope: Construct, id: string, props: MyConstructProps) {
super(scope, id);
const bucketName = props.bucketName;
}
}
const bucket = new Bucket(this, 'Bucket', {
bucketName: 'my-app-bucket',
});
const bucket = new Bucket(this, 'Bucket');
handler.addEnvironment('BUCKET_NAME', bucket.bucketName);
CDK Quick Reference
Commands:
- cdk init app --language typescript Initialize new app
- cdk synth Synthesize CloudFormation
- cdk diff Show pending changes
- cdk deploy Deploy stack
- cdk deploy --hotswap Fast deploy (dev only)
- cdk destroy Delete stack
- cdk bootstrap Bootstrap account/region
Construct Levels:
- L1 (CfnXxx) Raw CloudFormation, full control, verbose
- L2 (Xxx) Intent-based, sensible defaults, grants
- L3 (patterns) Multi-resource patterns, opinionated
Key Imports:
- aws-cdk-lib Core CDK library
- aws-cdk-lib/aws-lambda-nodejs NodejsFunction
- aws-cdk-lib/aws-lambda-event-sources Event source mappings
- aws-cdk-lib/pipelines CDK Pipelines
- aws-cdk-lib/assertions Testing utilities
- cdk-nag Security/compliance checks
NodejsFunction Runtimes:
- NODEJS_18_X Includes @aws-sdk/* (v3)
- NODEJS_20_X Recommended (LTS)
- NODEJS_22_X Latest features