432 lines
9.6 KiB
Markdown
432 lines
9.6 KiB
Markdown
# AWS CDK Patterns and Best Practices
|
|
|
|
This reference provides detailed patterns, anti-patterns, and best practices for AWS CDK development.
|
|
|
|
## Table of Contents
|
|
|
|
- [Naming Conventions](#naming-conventions)
|
|
- [Construct Patterns](#construct-patterns)
|
|
- [Security Patterns](#security-patterns)
|
|
- [Lambda Integration](#lambda-integration)
|
|
- [Testing Patterns](#testing-patterns)
|
|
- [Cost Optimization](#cost-optimization)
|
|
- [Anti-Patterns](#anti-patterns)
|
|
|
|
## Naming Conventions
|
|
|
|
### Automatic Resource Naming (Recommended)
|
|
|
|
Let CDK and CloudFormation generate unique resource names automatically:
|
|
|
|
**Benefits**:
|
|
- Enables multiple deployments in the same region/account
|
|
- Supports parallel environments (dev, staging, prod)
|
|
- Prevents naming conflicts
|
|
- Allows stack cloning and testing
|
|
|
|
**Example**:
|
|
```typescript
|
|
// ✅ GOOD - Automatic naming
|
|
const bucket = new s3.Bucket(this, 'DataBucket', {
|
|
// No bucketName specified
|
|
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
});
|
|
```
|
|
|
|
### When Explicit Naming is Required
|
|
|
|
Some scenarios require explicit names:
|
|
- Resources referenced by external systems
|
|
- Resources that must maintain consistent names across deployments
|
|
- Cross-stack references requiring stable names
|
|
|
|
**Pattern**: Use logical prefixes and environment suffixes
|
|
```typescript
|
|
// Only when absolutely necessary
|
|
const bucket = new s3.Bucket(this, 'DataBucket', {
|
|
bucketName: `${props.projectName}-data-${props.environment}`,
|
|
});
|
|
```
|
|
|
|
## Construct Patterns
|
|
|
|
### L3 Constructs (Patterns)
|
|
|
|
Prefer high-level patterns that encapsulate best practices:
|
|
|
|
```typescript
|
|
import * as patterns from 'aws-cdk-lib/aws-apigateway';
|
|
|
|
new patterns.LambdaRestApi(this, 'MyApi', {
|
|
handler: myFunction,
|
|
// Includes CloudWatch Logs, IAM roles, and API Gateway configuration
|
|
});
|
|
```
|
|
|
|
### Custom Constructs
|
|
|
|
Create reusable constructs for repeated patterns:
|
|
|
|
```typescript
|
|
export class ApiWithDatabase extends Construct {
|
|
public readonly api: apigateway.RestApi;
|
|
public readonly table: dynamodb.Table;
|
|
|
|
constructor(scope: Construct, id: string, props: ApiWithDatabaseProps) {
|
|
super(scope, id);
|
|
|
|
this.table = new dynamodb.Table(this, 'Table', {
|
|
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
|
|
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
|
|
});
|
|
|
|
const handler = new NodejsFunction(this, 'Handler', {
|
|
entry: props.handlerEntry,
|
|
environment: {
|
|
TABLE_NAME: this.table.tableName,
|
|
},
|
|
});
|
|
|
|
this.table.grantReadWriteData(handler);
|
|
|
|
this.api = new apigateway.LambdaRestApi(this, 'Api', {
|
|
handler,
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
## Security Patterns
|
|
|
|
### IAM Least Privilege
|
|
|
|
Use grant methods instead of broad policies:
|
|
|
|
```typescript
|
|
// ✅ GOOD - Specific grants
|
|
const table = new dynamodb.Table(this, 'Table', { /* ... */ });
|
|
const lambda = new lambda.Function(this, 'Function', { /* ... */ });
|
|
|
|
table.grantReadWriteData(lambda);
|
|
|
|
// ❌ BAD - Overly broad permissions
|
|
lambda.addToRolePolicy(new iam.PolicyStatement({
|
|
actions: ['dynamodb:*'],
|
|
resources: ['*'],
|
|
}));
|
|
```
|
|
|
|
### Secrets Management
|
|
|
|
Use Secrets Manager for sensitive data:
|
|
|
|
```typescript
|
|
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
|
|
|
|
const secret = new secretsmanager.Secret(this, 'DbPassword', {
|
|
generateSecretString: {
|
|
secretStringTemplate: JSON.stringify({ username: 'admin' }),
|
|
generateStringKey: 'password',
|
|
excludePunctuation: true,
|
|
},
|
|
});
|
|
|
|
// Grant read access to Lambda
|
|
secret.grantRead(myFunction);
|
|
```
|
|
|
|
### VPC Configuration
|
|
|
|
Follow VPC best practices:
|
|
|
|
```typescript
|
|
const vpc = new ec2.Vpc(this, 'Vpc', {
|
|
maxAzs: 2,
|
|
natGateways: 1, // Cost optimization: use 1 for dev, 2+ for prod
|
|
subnetConfiguration: [
|
|
{
|
|
name: 'Public',
|
|
subnetType: ec2.SubnetType.PUBLIC,
|
|
cidrMask: 24,
|
|
},
|
|
{
|
|
name: 'Private',
|
|
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
|
|
cidrMask: 24,
|
|
},
|
|
{
|
|
name: 'Isolated',
|
|
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
|
|
cidrMask: 24,
|
|
},
|
|
],
|
|
});
|
|
```
|
|
|
|
## Lambda Integration
|
|
|
|
### NodejsFunction (TypeScript/JavaScript)
|
|
|
|
```typescript
|
|
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
|
|
const fn = new NodejsFunction(this, 'Function', {
|
|
entry: 'src/handlers/process.ts',
|
|
handler: 'handler',
|
|
runtime: lambda.Runtime.NODEJS_20_X,
|
|
timeout: Duration.seconds(30),
|
|
memorySize: 512,
|
|
environment: {
|
|
TABLE_NAME: table.tableName,
|
|
},
|
|
bundling: {
|
|
minify: true,
|
|
sourceMap: true,
|
|
externalModules: ['@aws-sdk/*'], // Use AWS SDK from Lambda runtime
|
|
},
|
|
});
|
|
```
|
|
|
|
### PythonFunction
|
|
|
|
```typescript
|
|
import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha';
|
|
|
|
const fn = new PythonFunction(this, 'Function', {
|
|
entry: 'src/handlers',
|
|
index: 'process.py',
|
|
handler: 'handler',
|
|
runtime: lambda.Runtime.PYTHON_3_12,
|
|
timeout: Duration.seconds(30),
|
|
memorySize: 512,
|
|
});
|
|
```
|
|
|
|
### Lambda Layers
|
|
|
|
Share code across functions:
|
|
|
|
```typescript
|
|
const layer = new lambda.LayerVersion(this, 'CommonLayer', {
|
|
code: lambda.Code.fromAsset('layers/common'),
|
|
compatibleRuntimes: [lambda.Runtime.NODEJS_20_X],
|
|
description: 'Common utilities',
|
|
});
|
|
|
|
new NodejsFunction(this, 'Function', {
|
|
entry: 'src/handler.ts',
|
|
layers: [layer],
|
|
});
|
|
```
|
|
|
|
## Testing Patterns
|
|
|
|
### Snapshot Testing
|
|
|
|
```typescript
|
|
import { Template } from 'aws-cdk-lib/assertions';
|
|
|
|
test('Stack creates expected resources', () => {
|
|
const app = new cdk.App();
|
|
const stack = new MyStack(app, 'TestStack');
|
|
|
|
const template = Template.fromStack(stack);
|
|
expect(template.toJSON()).toMatchSnapshot();
|
|
});
|
|
```
|
|
|
|
### Fine-Grained Assertions
|
|
|
|
```typescript
|
|
test('Lambda has correct environment', () => {
|
|
const app = new cdk.App();
|
|
const stack = new MyStack(app, 'TestStack');
|
|
|
|
const template = Template.fromStack(stack);
|
|
|
|
template.hasResourceProperties('AWS::Lambda::Function', {
|
|
Runtime: 'nodejs20.x',
|
|
Timeout: 30,
|
|
Environment: {
|
|
Variables: {
|
|
TABLE_NAME: { Ref: Match.anyValue() },
|
|
},
|
|
},
|
|
});
|
|
});
|
|
```
|
|
|
|
### Resource Count Validation
|
|
|
|
```typescript
|
|
test('Stack has correct number of functions', () => {
|
|
const app = new cdk.App();
|
|
const stack = new MyStack(app, 'TestStack');
|
|
|
|
const template = Template.fromStack(stack);
|
|
template.resourceCountIs('AWS::Lambda::Function', 3);
|
|
});
|
|
```
|
|
|
|
## Cost Optimization
|
|
|
|
### Right-Sizing Lambda
|
|
|
|
```typescript
|
|
// Development
|
|
const devFunction = new NodejsFunction(this, 'DevFunction', {
|
|
memorySize: 256, // Lower for dev
|
|
timeout: Duration.seconds(30),
|
|
});
|
|
|
|
// Production
|
|
const prodFunction = new NodejsFunction(this, 'ProdFunction', {
|
|
memorySize: 1024, // Higher for prod performance
|
|
timeout: Duration.seconds(10),
|
|
reservedConcurrentExecutions: 10, // Prevent runaway costs
|
|
});
|
|
```
|
|
|
|
### DynamoDB Billing Modes
|
|
|
|
```typescript
|
|
// Development/Low Traffic
|
|
const devTable = new dynamodb.Table(this, 'DevTable', {
|
|
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
|
|
});
|
|
|
|
// Production/Predictable Load
|
|
const prodTable = new dynamodb.Table(this, 'ProdTable', {
|
|
billingMode: dynamodb.BillingMode.PROVISIONED,
|
|
readCapacity: 5,
|
|
writeCapacity: 5,
|
|
autoScaling: { /* ... */ },
|
|
});
|
|
```
|
|
|
|
### S3 Lifecycle Policies
|
|
|
|
```typescript
|
|
const bucket = new s3.Bucket(this, 'DataBucket', {
|
|
lifecycleRules: [
|
|
{
|
|
id: 'MoveToIA',
|
|
transitions: [
|
|
{
|
|
storageClass: s3.StorageClass.INFREQUENT_ACCESS,
|
|
transitionAfter: Duration.days(30),
|
|
},
|
|
{
|
|
storageClass: s3.StorageClass.GLACIER,
|
|
transitionAfter: Duration.days(90),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'CleanupOldVersions',
|
|
noncurrentVersionExpiration: Duration.days(30),
|
|
},
|
|
],
|
|
});
|
|
```
|
|
|
|
## Anti-Patterns
|
|
|
|
### ❌ Hardcoded Values
|
|
|
|
```typescript
|
|
// BAD
|
|
new lambda.Function(this, 'Function', {
|
|
functionName: 'my-function', // Prevents multiple deployments
|
|
code: lambda.Code.fromAsset('lambda'),
|
|
handler: 'index.handler',
|
|
runtime: lambda.Runtime.NODEJS_20_X,
|
|
});
|
|
|
|
// GOOD
|
|
new NodejsFunction(this, 'Function', {
|
|
entry: 'src/handler.ts',
|
|
// Let CDK generate the name
|
|
});
|
|
```
|
|
|
|
### ❌ Overly Broad IAM Permissions
|
|
|
|
```typescript
|
|
// BAD
|
|
function.addToRolePolicy(new iam.PolicyStatement({
|
|
actions: ['*'],
|
|
resources: ['*'],
|
|
}));
|
|
|
|
// GOOD
|
|
table.grantReadWriteData(function);
|
|
```
|
|
|
|
### ❌ Manual Dependency Management
|
|
|
|
```typescript
|
|
// BAD - Manual bundling
|
|
new lambda.Function(this, 'Function', {
|
|
code: lambda.Code.fromAsset('lambda.zip'), // Pre-bundled manually
|
|
// ...
|
|
});
|
|
|
|
// GOOD - Let CDK handle it
|
|
new NodejsFunction(this, 'Function', {
|
|
entry: 'src/handler.ts',
|
|
// CDK handles bundling automatically
|
|
});
|
|
```
|
|
|
|
### ❌ Missing Environment Variables
|
|
|
|
```typescript
|
|
// BAD
|
|
new NodejsFunction(this, 'Function', {
|
|
entry: 'src/handler.ts',
|
|
// Table name hardcoded in Lambda code
|
|
});
|
|
|
|
// GOOD
|
|
new NodejsFunction(this, 'Function', {
|
|
entry: 'src/handler.ts',
|
|
environment: {
|
|
TABLE_NAME: table.tableName,
|
|
},
|
|
});
|
|
```
|
|
|
|
### ❌ Ignoring Stack Outputs
|
|
|
|
```typescript
|
|
// BAD - No way to reference resources
|
|
new MyStack(app, 'Stack', {});
|
|
|
|
// GOOD - Export important values
|
|
class MyStack extends Stack {
|
|
constructor(scope: Construct, id: string) {
|
|
super(scope, id);
|
|
|
|
const api = new apigateway.RestApi(this, 'Api', {});
|
|
|
|
new CfnOutput(this, 'ApiUrl', {
|
|
value: api.url,
|
|
description: 'API Gateway URL',
|
|
exportName: 'MyApiUrl',
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
## Summary
|
|
|
|
- **Always** let CDK generate resource names unless explicitly required
|
|
- **Use** high-level constructs (L2/L3) over low-level (L1)
|
|
- **Prefer** grant methods for IAM permissions
|
|
- **Leverage** `NodejsFunction` and `PythonFunction` for automatic bundling
|
|
- **Test** stacks with assertions and snapshots
|
|
- **Optimize** costs based on environment (dev vs prod)
|
|
- **Validate** infrastructure before deployment
|
|
- **Document** custom constructs and patterns
|