Files
2025-11-30 09:08:46 +08:00

839 lines
20 KiB
Markdown

# Serverless Architecture Patterns
Comprehensive patterns for building serverless applications on AWS based on Well-Architected Framework principles.
## Table of Contents
- [Core Serverless Patterns](#core-serverless-patterns)
- [API Patterns](#api-patterns)
- [Data Processing Patterns](#data-processing-patterns)
- [Integration Patterns](#integration-patterns)
- [Orchestration Patterns](#orchestration-patterns)
- [Anti-Patterns](#anti-patterns)
## Core Serverless Patterns
### Pattern: Serverless Microservices
**Use case**: Independent, scalable services with separate databases
**Architecture**:
```
API Gateway → Lambda Functions → DynamoDB/RDS
↓ (events)
EventBridge → Other Services
```
**CDK Implementation**:
```typescript
// User Service
const userTable = new dynamodb.Table(this, 'Users', {
partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
});
const userFunction = new NodejsFunction(this, 'UserHandler', {
entry: 'src/services/users/handler.ts',
environment: {
TABLE_NAME: userTable.tableName,
},
});
userTable.grantReadWriteData(userFunction);
// Order Service (separate database)
const orderTable = new dynamodb.Table(this, 'Orders', {
partitionKey: { name: 'orderId', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
});
const orderFunction = new NodejsFunction(this, 'OrderHandler', {
entry: 'src/services/orders/handler.ts',
environment: {
TABLE_NAME: orderTable.tableName,
EVENT_BUS: eventBus.eventBusName,
},
});
orderTable.grantReadWriteData(orderFunction);
eventBus.grantPutEventsTo(orderFunction);
```
**Benefits**:
- Independent deployment and scaling
- Database per service (data isolation)
- Technology diversity
- Fault isolation
### Pattern: Serverless API Backend
**Use case**: REST or GraphQL API with serverless compute
**REST API with API Gateway**:
```typescript
const api = new apigateway.RestApi(this, 'Api', {
restApiName: 'serverless-api',
deployOptions: {
stageName: 'prod',
tracingEnabled: true,
loggingLevel: apigateway.MethodLoggingLevel.INFO,
dataTraceEnabled: true,
metricsEnabled: true,
},
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS,
allowMethods: apigateway.Cors.ALL_METHODS,
},
});
// Resource-based routing
const items = api.root.addResource('items');
items.addMethod('GET', new apigateway.LambdaIntegration(listFunction));
items.addMethod('POST', new apigateway.LambdaIntegration(createFunction));
const item = items.addResource('{id}');
item.addMethod('GET', new apigateway.LambdaIntegration(getFunction));
item.addMethod('PUT', new apigateway.LambdaIntegration(updateFunction));
item.addMethod('DELETE', new apigateway.LambdaIntegration(deleteFunction));
```
**GraphQL API with AppSync**:
```typescript
const api = new appsync.GraphqlApi(this, 'Api', {
name: 'serverless-graphql-api',
schema: appsync.SchemaFile.fromAsset('schema.graphql'),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.API_KEY,
},
},
xrayEnabled: true,
});
// Lambda resolver
const dataSource = api.addLambdaDataSource('lambda-ds', resolverFunction);
dataSource.createResolver('QueryGetItem', {
typeName: 'Query',
fieldName: 'getItem',
});
```
### Pattern: Serverless Data Lake
**Use case**: Ingest, process, and analyze large-scale data
**Architecture**:
```
S3 (raw data) → Lambda (transform) → S3 (processed)
↓ (catalog)
AWS Glue → Athena (query)
```
**Implementation**:
```typescript
const rawBucket = new s3.Bucket(this, 'RawData');
const processedBucket = new s3.Bucket(this, 'ProcessedData');
// Trigger Lambda on file upload
rawBucket.addEventNotification(
s3.EventType.OBJECT_CREATED,
new s3n.LambdaDestination(transformFunction),
{ prefix: 'incoming/' }
);
// Transform function
export const transform = async (event: S3Event) => {
for (const record of event.Records) {
const key = record.s3.object.key;
// Get raw data
const raw = await s3.getObject({
Bucket: record.s3.bucket.name,
Key: key,
});
// Transform data
const transformed = await transformData(raw.Body);
// Write to processed bucket
await s3.putObject({
Bucket: process.env.PROCESSED_BUCKET,
Key: `processed/${key}`,
Body: JSON.stringify(transformed),
});
}
};
```
## API Patterns
### Pattern: Authorizer Pattern
**Use case**: Custom authentication and authorization
```typescript
// Lambda authorizer
const authorizer = new apigateway.TokenAuthorizer(this, 'Authorizer', {
handler: authorizerFunction,
identitySource: 'method.request.header.Authorization',
resultsCacheTtl: Duration.minutes(5),
});
// Apply to API methods
const resource = api.root.addResource('protected');
resource.addMethod('GET', new apigateway.LambdaIntegration(protectedFunction), {
authorizer,
});
```
### Pattern: Request Validation
**Use case**: Validate requests before Lambda invocation
```typescript
const requestModel = api.addModel('RequestModel', {
contentType: 'application/json',
schema: {
type: apigateway.JsonSchemaType.OBJECT,
required: ['name', 'email'],
properties: {
name: { type: apigateway.JsonSchemaType.STRING, minLength: 1 },
email: { type: apigateway.JsonSchemaType.STRING, format: 'email' },
},
},
});
resource.addMethod('POST', integration, {
requestValidator: new apigateway.RequestValidator(this, 'Validator', {
api,
validateRequestBody: true,
validateRequestParameters: true,
}),
requestModels: {
'application/json': requestModel,
},
});
```
### Pattern: Response Caching
**Use case**: Reduce backend load and improve latency
```typescript
const api = new apigateway.RestApi(this, 'Api', {
deployOptions: {
cachingEnabled: true,
cacheTtl: Duration.minutes(5),
cacheClusterEnabled: true,
cacheClusterSize: '0.5', // GB
},
});
// Enable caching per method
resource.addMethod('GET', integration, {
methodResponses: [{
statusCode: '200',
responseParameters: {
'method.response.header.Cache-Control': true,
},
}],
});
```
## Data Processing Patterns
### Pattern: S3 Event Processing
**Use case**: Process files uploaded to S3
```typescript
const bucket = new s3.Bucket(this, 'DataBucket');
// Process images
bucket.addEventNotification(
s3.EventType.OBJECT_CREATED,
new s3n.LambdaDestination(imageProcessingFunction),
{ suffix: '.jpg' }
);
// Process CSV files
bucket.addEventNotification(
s3.EventType.OBJECT_CREATED,
new s3n.LambdaDestination(csvProcessingFunction),
{ suffix: '.csv' }
);
// Large file processing with Step Functions
bucket.addEventNotification(
s3.EventType.OBJECT_CREATED,
new s3n.SfnDestination(processingStateMachine),
{ prefix: 'large-files/' }
);
```
### Pattern: DynamoDB Streams Processing
**Use case**: React to database changes
```typescript
const table = new dynamodb.Table(this, 'Table', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
});
// Process stream changes
new lambda.EventSourceMapping(this, 'StreamConsumer', {
target: streamProcessorFunction,
eventSourceArn: table.tableStreamArn,
startingPosition: lambda.StartingPosition.LATEST,
batchSize: 100,
maxBatchingWindow: Duration.seconds(5),
bisectBatchOnError: true,
retryAttempts: 3,
});
// Example: Sync to search index
export const processStream = async (event: DynamoDBStreamEvent) => {
for (const record of event.Records) {
if (record.eventName === 'INSERT' || record.eventName === 'MODIFY') {
const newImage = record.dynamodb?.NewImage;
await elasticSearch.index({
index: 'items',
id: newImage?.id.S,
body: unmarshall(newImage),
});
} else if (record.eventName === 'REMOVE') {
await elasticSearch.delete({
index: 'items',
id: record.dynamodb?.Keys?.id.S,
});
}
}
};
```
### Pattern: Kinesis Stream Processing
**Use case**: Real-time data streaming and analytics
```typescript
const stream = new kinesis.Stream(this, 'EventStream', {
shardCount: 2,
streamMode: kinesis.StreamMode.PROVISIONED,
});
// Fan-out with multiple consumers
const consumer1 = new lambda.EventSourceMapping(this, 'Analytics', {
target: analyticsFunction,
eventSourceArn: stream.streamArn,
startingPosition: lambda.StartingPosition.LATEST,
batchSize: 100,
parallelizationFactor: 10, // Process 10 batches per shard in parallel
});
const consumer2 = new lambda.EventSourceMapping(this, 'Alerting', {
target: alertingFunction,
eventSourceArn: stream.streamArn,
startingPosition: lambda.StartingPosition.LATEST,
filters: [
lambda.FilterCriteria.filter({
eventName: lambda.FilterRule.isEqual('CRITICAL_EVENT'),
}),
],
});
```
## Integration Patterns
### Pattern: Service Integration with EventBridge
**Use case**: Decouple services with events
```typescript
const eventBus = new events.EventBus(this, 'AppBus');
// Service A publishes events
const serviceA = new NodejsFunction(this, 'ServiceA', {
entry: 'src/services/a/handler.ts',
environment: {
EVENT_BUS: eventBus.eventBusName,
},
});
eventBus.grantPutEventsTo(serviceA);
// Service B subscribes to events
new events.Rule(this, 'ServiceBRule', {
eventBus,
eventPattern: {
source: ['service.a'],
detailType: ['EntityCreated'],
},
targets: [new targets.LambdaFunction(serviceBFunction)],
});
// Service C subscribes to same events
new events.Rule(this, 'ServiceCRule', {
eventBus,
eventPattern: {
source: ['service.a'],
detailType: ['EntityCreated'],
},
targets: [new targets.LambdaFunction(serviceCFunction)],
});
```
### Pattern: API Gateway + SQS Integration
**Use case**: Async API requests without Lambda
```typescript
const queue = new sqs.Queue(this, 'RequestQueue');
const api = new apigateway.RestApi(this, 'Api');
// Direct SQS integration (no Lambda)
const sqsIntegration = new apigateway.AwsIntegration({
service: 'sqs',
path: `${process.env.AWS_ACCOUNT_ID}/${queue.queueName}`,
integrationHttpMethod: 'POST',
options: {
credentialsRole: sqsRole,
requestParameters: {
'integration.request.header.Content-Type': "'application/x-www-form-urlencoded'",
},
requestTemplates: {
'application/json': 'Action=SendMessage&MessageBody=$input.body',
},
integrationResponses: [{
statusCode: '200',
}],
},
});
api.root.addMethod('POST', sqsIntegration, {
methodResponses: [{ statusCode: '200' }],
});
```
### Pattern: EventBridge + Step Functions
**Use case**: Event-triggered workflow orchestration
```typescript
// State machine for order processing
const orderStateMachine = new stepfunctions.StateMachine(this, 'OrderFlow', {
definition: /* ... */,
});
// EventBridge triggers state machine
new events.Rule(this, 'OrderPlacedRule', {
eventPattern: {
source: ['orders'],
detailType: ['OrderPlaced'],
},
targets: [new targets.SfnStateMachine(orderStateMachine)],
});
```
## Orchestration Patterns
### Pattern: Sequential Workflow
**Use case**: Multi-step process with dependencies
```typescript
const definition = new tasks.LambdaInvoke(this, 'Step1', {
lambdaFunction: step1Function,
outputPath: '$.Payload',
})
.next(new tasks.LambdaInvoke(this, 'Step2', {
lambdaFunction: step2Function,
outputPath: '$.Payload',
}))
.next(new tasks.LambdaInvoke(this, 'Step3', {
lambdaFunction: step3Function,
outputPath: '$.Payload',
}));
new stepfunctions.StateMachine(this, 'Sequential', {
definition,
});
```
### Pattern: Parallel Execution
**Use case**: Execute independent tasks concurrently
```typescript
const parallel = new stepfunctions.Parallel(this, 'ParallelProcessing');
parallel.branch(new tasks.LambdaInvoke(this, 'ProcessA', {
lambdaFunction: functionA,
}));
parallel.branch(new tasks.LambdaInvoke(this, 'ProcessB', {
lambdaFunction: functionB,
}));
parallel.branch(new tasks.LambdaInvoke(this, 'ProcessC', {
lambdaFunction: functionC,
}));
const definition = parallel.next(new tasks.LambdaInvoke(this, 'Aggregate', {
lambdaFunction: aggregateFunction,
}));
new stepfunctions.StateMachine(this, 'Parallel', { definition });
```
### Pattern: Map State (Dynamic Parallelism)
**Use case**: Process array of items in parallel
```typescript
const mapState = new stepfunctions.Map(this, 'ProcessItems', {
maxConcurrency: 10,
itemsPath: '$.items',
});
mapState.iterator(new tasks.LambdaInvoke(this, 'ProcessItem', {
lambdaFunction: processItemFunction,
}));
const definition = mapState.next(new tasks.LambdaInvoke(this, 'Finalize', {
lambdaFunction: finalizeFunction,
}));
```
### Pattern: Choice State (Conditional Logic)
**Use case**: Branching logic based on input
```typescript
const choice = new stepfunctions.Choice(this, 'OrderType');
choice.when(
stepfunctions.Condition.stringEquals('$.orderType', 'STANDARD'),
standardProcessing
);
choice.when(
stepfunctions.Condition.stringEquals('$.orderType', 'EXPRESS'),
expressProcessing
);
choice.otherwise(defaultProcessing);
```
### Pattern: Wait State
**Use case**: Delay between steps or wait for callbacks
```typescript
// Fixed delay
const wait = new stepfunctions.Wait(this, 'Wait30Seconds', {
time: stepfunctions.WaitTime.duration(Duration.seconds(30)),
});
// Wait until timestamp
const waitUntil = new stepfunctions.Wait(this, 'WaitUntil', {
time: stepfunctions.WaitTime.timestampPath('$.expiryTime'),
});
// Wait for callback (.waitForTaskToken)
const waitForCallback = new tasks.LambdaInvoke(this, 'WaitForApproval', {
lambdaFunction: approvalFunction,
integrationPattern: stepfunctions.IntegrationPattern.WAIT_FOR_TASK_TOKEN,
payload: stepfunctions.TaskInput.fromObject({
token: stepfunctions.JsonPath.taskToken,
data: stepfunctions.JsonPath.entirePayload,
}),
});
```
## Anti-Patterns
### ❌ Lambda Monolith
**Problem**: Single Lambda handling all operations
```typescript
// BAD
export const handler = async (event: any) => {
switch (event.operation) {
case 'createUser': return createUser(event);
case 'getUser': return getUser(event);
case 'updateUser': return updateUser(event);
case 'deleteUser': return deleteUser(event);
case 'createOrder': return createOrder(event);
// ... 20 more operations
}
};
```
**Solution**: Separate Lambda functions per operation
```typescript
// GOOD - Separate functions
export const createUser = async (event: any) => { /* ... */ };
export const getUser = async (event: any) => { /* ... */ };
export const updateUser = async (event: any) => { /* ... */ };
```
### ❌ Recursive Lambda Pattern
**Problem**: Lambda invoking itself (runaway costs)
```typescript
// BAD
export const handler = async (event: any) => {
await processItem(event);
if (hasMoreItems()) {
await lambda.invoke({
FunctionName: process.env.AWS_LAMBDA_FUNCTION_NAME,
InvocationType: 'Event',
Payload: JSON.stringify({ /* next batch */ }),
});
}
};
```
**Solution**: Use SQS or Step Functions
```typescript
// GOOD - Use SQS for iteration
export const handler = async (event: SQSEvent) => {
for (const record of event.Records) {
await processItem(record);
}
// SQS handles iteration automatically
};
```
### ❌ Lambda Chaining
**Problem**: Lambda directly invoking another Lambda
```typescript
// BAD
export const handler1 = async (event: any) => {
const result = await processStep1(event);
// Directly invoking next Lambda
await lambda.invoke({
FunctionName: 'handler2',
Payload: JSON.stringify(result),
});
};
```
**Solution**: Use EventBridge, SQS, or Step Functions
```typescript
// GOOD - Publish to EventBridge
export const handler1 = async (event: any) => {
const result = await processStep1(event);
await eventBridge.putEvents({
Entries: [{
Source: 'service.step1',
DetailType: 'Step1Completed',
Detail: JSON.stringify(result),
}],
});
};
```
### ❌ Synchronous Waiting in Lambda
**Problem**: Lambda waiting for slow operations
```typescript
// BAD - Blocking on slow operation
export const handler = async (event: any) => {
await startBatchJob(); // Returns immediately
// Wait for job to complete (wastes Lambda time)
while (true) {
const status = await checkJobStatus();
if (status === 'COMPLETE') break;
await sleep(1000);
}
};
```
**Solution**: Use Step Functions with callback pattern
```typescript
// GOOD - Step Functions waits, not Lambda
const waitForJob = new tasks.LambdaInvoke(this, 'StartJob', {
lambdaFunction: startJobFunction,
integrationPattern: stepfunctions.IntegrationPattern.WAIT_FOR_TASK_TOKEN,
payload: stepfunctions.TaskInput.fromObject({
token: stepfunctions.JsonPath.taskToken,
}),
});
```
### ❌ Large Deployment Packages
**Problem**: Large Lambda packages increase cold start time
**Solution**:
- Use layers for shared dependencies
- Externalize AWS SDK
- Minimize bundle size
```typescript
new NodejsFunction(this, 'Function', {
entry: 'src/handler.ts',
bundling: {
minify: true,
externalModules: ['@aws-sdk/*'], // Provided by runtime
nodeModules: ['only-needed-deps'], // Selective bundling
},
});
```
## Performance Optimization
### Cold Start Optimization
**Techniques**:
1. Minimize package size
2. Use provisioned concurrency for critical paths
3. Lazy load dependencies
4. Reuse connections outside handler
5. Use Lambda SnapStart (Java)
```typescript
// For latency-sensitive APIs
const apiFunction = new NodejsFunction(this, 'ApiFunction', {
entry: 'src/api.ts',
memorySize: 1769, // 1 vCPU for faster initialization
});
const alias = apiFunction.currentVersion.addAlias('live');
alias.addAutoScaling({
minCapacity: 2,
maxCapacity: 10,
}).scaleOnUtilization({
utilizationTarget: 0.7,
});
```
### Right-Sizing Memory
**Test different memory configurations**:
```typescript
// CPU-bound workload
new NodejsFunction(this, 'ComputeFunction', {
memorySize: 1769, // 1 vCPU
timeout: Duration.seconds(30),
});
// I/O-bound workload
new NodejsFunction(this, 'IOFunction', {
memorySize: 512, // Less CPU needed
timeout: Duration.seconds(60),
});
// Simple operations
new NodejsFunction(this, 'SimpleFunction', {
memorySize: 256,
timeout: Duration.seconds(10),
});
```
### Concurrent Execution Control
```typescript
// Protect downstream services
new NodejsFunction(this, 'Function', {
reservedConcurrentExecutions: 10, // Max 10 concurrent
});
// Unreserved concurrency (shared pool)
new NodejsFunction(this, 'Function', {
// Uses unreserved account concurrency
});
```
## Testing Strategies
### Unit Testing
Test business logic separate from AWS services:
```typescript
// handler.ts
export const processOrder = async (order: Order): Promise<Result> => {
// Business logic (easily testable)
const validated = validateOrder(order);
const priced = calculatePrice(validated);
return transformResult(priced);
};
export const handler = async (event: any): Promise<any> => {
const order = parseEvent(event);
const result = await processOrder(order);
await saveToDatabase(result);
return formatResponse(result);
};
// handler.test.ts
test('processOrder calculates price correctly', () => {
const order = { items: [{ price: 10, quantity: 2 }] };
const result = processOrder(order);
expect(result.total).toBe(20);
});
```
### Integration Testing
Test with actual AWS services:
```typescript
// integration.test.ts
import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda';
test('Lambda processes order correctly', async () => {
const lambda = new LambdaClient({});
const response = await lambda.send(new InvokeCommand({
FunctionName: process.env.FUNCTION_NAME,
Payload: JSON.stringify({ orderId: '123' }),
}));
const result = JSON.parse(Buffer.from(response.Payload!).toString());
expect(result.statusCode).toBe(200);
});
```
### Local Testing with SAM
```bash
# Test API locally
sam local start-api
# Invoke function locally
sam local invoke MyFunction -e events/test-event.json
# Generate sample event
sam local generate-event apigateway aws-proxy > event.json
```
## Summary
- **Single Purpose**: One function, one responsibility
- **Concurrent Design**: Think concurrency, not volume
- **Stateless**: Use external storage for state
- **State Machines**: Orchestrate with Step Functions
- **Event-Driven**: Use events over direct calls
- **Idempotent**: Handle failures and duplicates gracefully
- **Observability**: Enable tracing and structured logging