Initial commit
This commit is contained in:
431
skills/aws-cdk-development/references/cdk-patterns.md
Normal file
431
skills/aws-cdk-development/references/cdk-patterns.md
Normal file
@@ -0,0 +1,431 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user