6.7 KiB
6.7 KiB
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
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
// 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)
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)
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.KMSfor 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
// 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.
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