196 lines
6.7 KiB
Markdown
196 lines
6.7 KiB
Markdown
# AWS CDK Best Practices
|
|
|
|
## Purpose
|
|
Guidelines for building maintainable, secure, and testable infrastructure using the AWS Cloud Development Kit. These practices apply to CDK v2 with TypeScript (the recommended language for most teams).
|
|
|
|
## Construct Levels
|
|
|
|
### L1 (Cfn Resources)
|
|
- Direct CloudFormation resource wrappers (e.g., `CfnBucket`)
|
|
- Use only when L2 constructs do not expose a needed property
|
|
- Require manual configuration of all properties (no defaults)
|
|
|
|
### L2 (Curated Constructs)
|
|
- AWS-maintained constructs with sensible defaults (e.g., `Bucket`, `Function`, `Table`)
|
|
- Include helper methods (e.g., `bucket.grantRead(lambda)`)
|
|
- Preferred for most use cases — they encode AWS best practices
|
|
|
|
### L3 (Patterns)
|
|
- Higher-level constructs combining multiple resources (e.g., `LambdaRestApi`)
|
|
- Use when the pattern fits your needs exactly
|
|
- Avoid if you need significant customization — drop down to L2 instead
|
|
|
|
## Construct Design Patterns
|
|
|
|
### Single Responsibility
|
|
Each custom construct should represent one logical unit (a service, a data pipeline stage, a monitoring stack). Do not create constructs that build unrelated resources.
|
|
|
|
### Props Interface Pattern
|
|
```typescript
|
|
export interface OrderServiceProps {
|
|
readonly vpc: ec2.IVpc;
|
|
readonly table: dynamodb.ITable;
|
|
readonly environment: string; // 'dev' | 'staging' | 'prod'
|
|
readonly alarmTopic?: sns.ITopic; // optional props use ?
|
|
}
|
|
|
|
export class OrderService extends Construct {
|
|
public readonly api: apigateway.RestApi; // expose outputs as public readonly
|
|
|
|
constructor(scope: Construct, id: string, props: OrderServiceProps) {
|
|
super(scope, id);
|
|
// ...
|
|
}
|
|
}
|
|
```
|
|
|
|
### Rules
|
|
- Accept dependencies via props (dependency injection), do not create shared resources inside constructs
|
|
- Use interface types (`IVpc`, `ITable`) for props, not concrete types — enables cross-stack references
|
|
- Expose outputs as public readonly properties for consuming constructs
|
|
- Prefix optional props with documentation explaining the default behavior
|
|
|
|
## Stack Organization
|
|
|
|
### Recommended Structure
|
|
```
|
|
/infrastructure
|
|
/bin
|
|
app.ts # CDK app entry point, environment configuration
|
|
/lib
|
|
/constructs # Reusable L3 constructs
|
|
order-service.ts
|
|
monitoring.ts
|
|
/stacks
|
|
network-stack.ts # VPC, subnets, security groups
|
|
data-stack.ts # DynamoDB, S3, RDS
|
|
compute-stack.ts # Lambda, ECS, API Gateway
|
|
monitoring-stack.ts # CloudWatch, alarms, dashboards
|
|
/test
|
|
order-service.test.ts
|
|
data-stack.test.ts
|
|
```
|
|
|
|
### Stack Separation Guidelines
|
|
- Separate stacks by lifecycle: resources that change together should be in the same stack
|
|
- Stateful resources (databases, S3 buckets) in separate stacks from stateless (Lambda, API Gateway)
|
|
- Stateful stacks change rarely; stateless stacks deploy frequently
|
|
- Use cross-stack references sparingly — they create deployment coupling
|
|
|
|
## Environment-Aware Stacks
|
|
|
|
### Pattern
|
|
```typescript
|
|
// bin/app.ts
|
|
const app = new cdk.App();
|
|
const env = app.node.tryGetContext('env') || 'dev';
|
|
|
|
const config = {
|
|
dev: { instanceType: 't3.small', minCapacity: 1, maxCapacity: 2 },
|
|
staging: { instanceType: 't3.medium', minCapacity: 2, maxCapacity: 4 },
|
|
prod: { instanceType: 't3.large', minCapacity: 3, maxCapacity: 10 },
|
|
}[env];
|
|
|
|
new ComputeStack(app, `ComputeStack-${env}`, {
|
|
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-1' },
|
|
config,
|
|
});
|
|
```
|
|
|
|
### Rules
|
|
- Never hardcode account IDs or regions — use environment variables or context
|
|
- Use the same code for all environments; parameterize differences through config
|
|
- Production stacks must specify explicit `env` (account + region) — do not rely on defaults
|
|
|
|
## CDK Testing
|
|
|
|
### Assertion Tests (Fine-Grained)
|
|
```typescript
|
|
test('DynamoDB table has encryption enabled', () => {
|
|
const app = new cdk.App();
|
|
const stack = new DataStack(app, 'TestStack');
|
|
const template = Template.fromStack(stack);
|
|
|
|
template.hasResourceProperties('AWS::DynamoDB::Table', {
|
|
SSESpecification: {
|
|
SSEEnabled: true,
|
|
},
|
|
});
|
|
});
|
|
```
|
|
|
|
### Snapshot Tests (Regression Detection)
|
|
```typescript
|
|
test('stack matches snapshot', () => {
|
|
const app = new cdk.App();
|
|
const stack = new DataStack(app, 'TestStack');
|
|
const template = Template.fromStack(stack);
|
|
expect(template.toJSON()).toMatchSnapshot();
|
|
});
|
|
```
|
|
- Update snapshots intentionally (`jest --updateSnapshot`) after deliberate changes
|
|
- Review snapshot diffs in pull requests — they show exactly what infrastructure changes
|
|
|
|
### What to Test
|
|
- Security properties: encryption enabled, public access blocked, least-privilege policies
|
|
- Critical configuration: retention policies, backup settings, auto-scaling parameters
|
|
- Resource counts: expected number of Lambda functions, tables, queues
|
|
- Do NOT test CDK internals or CloudFormation implementation details
|
|
|
|
## Security Defaults
|
|
|
|
### Encryption
|
|
- S3: `encryption: s3.BucketEncryption.S3_MANAGED` (minimum) or KMS for sensitive data
|
|
- DynamoDB: `encryption: dynamodb.TableEncryption.AWS_MANAGED` (default) or customer-managed KMS
|
|
- SQS: `encryption: sqs.QueueEncryption.KMS` for sensitive message content
|
|
- EBS: Enable encryption by default in account settings
|
|
|
|
### Access Control
|
|
- S3: `blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL` (always, unless serving public static content)
|
|
- Lambda: Use `grant*` methods instead of writing IAM policies manually
|
|
- API Gateway: Add authorization on every route (IAM, Cognito, or Lambda authorizer)
|
|
|
|
### Logging
|
|
- S3: Enable access logging to a dedicated logging bucket
|
|
- API Gateway: Enable access logging and execution logging
|
|
- Lambda: Logs go to CloudWatch automatically; set retention (`logRetention: logs.RetentionDays.ONE_MONTH`)
|
|
|
|
### Least Privilege
|
|
```typescript
|
|
// Good: specific grant
|
|
table.grantReadData(lambdaFunction);
|
|
|
|
// Bad: overly broad
|
|
lambdaFunction.addToRolePolicy(new iam.PolicyStatement({
|
|
actions: ['dynamodb:*'],
|
|
resources: ['*'],
|
|
}));
|
|
```
|
|
|
|
## CDK Aspects for Compliance
|
|
|
|
### Purpose
|
|
Aspects visit every construct in the tree and can validate, warn, or modify resources.
|
|
|
|
```typescript
|
|
class EncryptionChecker implements cdk.IAspect {
|
|
public visit(node: IConstruct): void {
|
|
if (node instanceof s3.CfnBucket) {
|
|
if (!node.bucketEncryption) {
|
|
Annotations.of(node).addError('S3 bucket must have encryption enabled');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply to the entire app
|
|
Aspects.of(app).add(new EncryptionChecker());
|
|
```
|
|
|
|
### Common Compliance Aspects
|
|
- Verify all S3 buckets have encryption and block public access
|
|
- Verify all DynamoDB tables have point-in-time recovery enabled
|
|
- Verify all Lambda functions have reserved concurrency set
|
|
- Verify all security groups do not allow 0.0.0.0/0 ingress
|
|
- Tag all resources with required cost allocation tags
|