AWS CDK
Language Overview¶
AWS Cloud Development Kit (CDK) is an infrastructure as code framework that lets you define cloud resources using familiar programming languages. This guide focuses on TypeScript CDK, covering best practices for creating maintainable, reusable infrastructure code.
Key Characteristics¶
- Languages: TypeScript (preferred), Python, Java, C#, Go
- Primary Use: Infrastructure as code on AWS
- Key Concepts: Apps, Stacks, Constructs, Props
- Version: CDK v2 (recommended)
Quick Reference¶
| Category | Convention | Example | Notes |
|---|---|---|---|
| Naming | |||
| Apps | PascalCase |
MyInfraApp |
CDK application class |
| Stacks | PascalCase |
VpcStack, DatabaseStack |
Stack class names |
| Constructs | PascalCase |
ApiGateway, LambdaFunction |
Custom construct classes |
| Props Interfaces | PascalCaseProps |
VpcStackProps, ApiProps |
Props interface suffix |
| Resources | camelCase |
myBucket, userTable |
Resource variables |
| File Naming | |||
| App Entry | bin/app-name.ts |
bin/my-app.ts |
Application entry point |
| Stacks | lib/stack-name-stack.ts |
lib/vpc-stack.ts |
Stack definitions |
| Constructs | lib/construct-name.ts |
lib/api-gateway.ts |
Reusable constructs |
| Key Concepts | |||
| App | Top-level container | new cdk.App() |
CDK application |
| Stack | Deployment unit | new cdk.Stack(app, 'MyStack') |
CloudFormation stack |
| Construct | Reusable component | Custom infrastructure patterns | Building blocks |
| Props | Configuration | Interfaces for construct config | Type-safe configuration |
| Best Practices | |||
| CDK v2 | Use CDK v2 | aws-cdk-lib |
Single package |
| TypeScript | Preferred language | Type safety, IDE support | Better developer experience |
| Constructs | L3 > L2 > L1 | Use higher-level constructs | Opinionated patterns |
| Environment | Pass explicitly | env: { account, region } |
Avoid implicit environments |
| Props | Required vs optional | Use TypeScript optionals | Clear interfaces |
| Common Patterns | |||
| Stacks | One stack per env | VpcStack, AppStack |
Logical separation |
| Cross-Stack Refs | Export/import | stack.export() |
Share resources |
| Context | Use cdk.json | Configuration values | Environment-specific config |
Project Structure¶
Basic CDK Project¶
my-cdk-app/
├── bin/
│ └── my-cdk-app.ts # App entry point
├── lib/
│ ├── my-cdk-app-stack.ts # Stack definitions
│ ├── constructs/ # Custom constructs
│ │ ├── api-construct.ts
│ │ └── database-construct.ts
│ └── config/ # Configuration
│ ├── dev.ts
│ └── prod.ts
├── test/
│ └── my-cdk-app.test.ts # Tests
├── cdk.json # CDK configuration
├── package.json
└── tsconfig.json
Basic Stack¶
Simple Stack Definition¶
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
export class MyStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Create S3 bucket
new s3.Bucket(this, 'MyBucket', {
bucketName: 'my-app-bucket',
versioned: true,
encryption: s3.BucketEncryption.S3_MANAGED,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
}
}
App Entry Point¶
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { MyStack } from '../lib/my-cdk-app-stack';
const app = new cdk.App();
new MyStack(app, 'MyStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
tags: {
Environment: 'production',
ManagedBy: 'CDK',
},
});
app.synth();
Constructs¶
L1 Constructs (CloudFormation Resources)¶
import * as cdk from 'aws-cdk-lib';
// Raw CloudFormation resource
const cfnBucket = new cdk.aws_s3.CfnBucket(this, 'MyCfnBucket', {
bucketName: 'my-cfn-bucket',
versioningConfiguration: {
status: 'Enabled',
},
});
L2 Constructs (AWS Constructs - Preferred)¶
import * as s3 from 'aws-cdk-lib/aws-s3';
const bucket = new s3.Bucket(this, 'MyBucket', {
bucketName: 'my-app-bucket',
versioned: true,
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
L3 Constructs (Custom Patterns)¶
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
export interface StaticWebsiteProps {
domainName: string;
certificateArn: string;
}
export class StaticWebsite extends Construct {
public readonly bucket: s3.Bucket;
public readonly distribution: cloudfront.Distribution;
constructor(scope: Construct, id: string, props: StaticWebsiteProps) {
super(scope, id);
// S3 bucket for website content
this.bucket = new s3.Bucket(this, 'WebsiteBucket', {
websiteIndexDocument: 'index.html',
websiteErrorDocument: 'error.html',
publicReadAccess: false,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
// CloudFront distribution
this.distribution = new cloudfront.Distribution(this, 'Distribution', {
defaultBehavior: {
origin: new origins.S3Origin(this.bucket),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
domainNames: [props.domainName],
certificate: acm.Certificate.fromCertificateArn(
this,
'Certificate',
props.certificateArn
),
});
}
}
Common Patterns¶
VPC Stack¶
import * as ec2 from 'aws-cdk-lib/aws-ec2';
export class NetworkStack extends cdk.Stack {
public readonly vpc: ec2.Vpc;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
this.vpc = new ec2.Vpc(this, 'VPC', {
maxAzs: 3,
natGateways: 1,
subnetConfiguration: [
{
cidrMask: 24,
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: 'Private',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
{
cidrMask: 28,
name: 'Isolated',
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
],
});
// Add VPC Flow Logs
this.vpc.addFlowLog('FlowLog', {
destination: ec2.FlowLogDestination.toCloudWatchLogs(),
});
}
}
RDS Database Stack¶
import * as rds from 'aws-cdk-lib/aws-rds';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
export class DatabaseStack extends cdk.Stack {
public readonly database: rds.DatabaseInstance;
constructor(scope: Construct, id: string, vpc: ec2.IVpc, props?: cdk.StackProps) {
super(scope, id, props);
// Security group
const dbSecurityGroup = new ec2.SecurityGroup(this, 'DatabaseSG', {
vpc,
description: 'Security group for RDS database',
allowAllOutbound: false,
});
// Database credentials
const dbCredentials = new secretsmanager.Secret(this, 'DBCredentials', {
generateSecretString: {
secretStringTemplate: JSON.stringify({ username: 'admin' }),
generateStringKey: 'password',
excludePunctuation: true,
},
});
// RDS instance
this.database = new rds.DatabaseInstance(this, 'Database', {
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_15_3,
}),
instanceType: ec2.InstanceType.of(
ec2.InstanceClass.T3,
ec2.InstanceSize.MICRO
),
vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
securityGroups: [dbSecurityGroup],
credentials: rds.Credentials.fromSecret(dbCredentials),
multiAz: true,
allocatedStorage: 100,
maxAllocatedStorage: 200,
backupRetention: cdk.Duration.days(7),
deletionProtection: true,
removalPolicy: cdk.RemovalPolicy.SNAPSHOT,
});
}
}
Lambda + API Gateway Stack¶
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as logs from 'aws-cdk-lib/aws-logs';
export class ApiStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Lambda function
const handler = new lambda.Function(this, 'ApiHandler', {
runtime: lambda.Runtime.NODEJS_18_X,
code: lambda.Code.fromAsset('lambda'),
handler: 'index.handler',
environment: {
TABLE_NAME: 'my-table',
},
timeout: cdk.Duration.seconds(30),
memorySize: 512,
logRetention: logs.RetentionDays.ONE_WEEK,
});
// API Gateway
const api = new apigateway.RestApi(this, 'Api', {
restApiName: 'My API',
description: 'API Gateway for my application',
deployOptions: {
stageName: 'prod',
loggingLevel: apigateway.MethodLoggingLevel.INFO,
dataTraceEnabled: true,
},
});
const integration = new apigateway.LambdaIntegration(handler);
// Add resources and methods
const items = api.root.addResource('items');
items.addMethod('GET', integration);
items.addMethod('POST', integration);
const item = items.addResource('{id}');
item.addMethod('GET', integration);
item.addMethod('PUT', integration);
item.addMethod('DELETE', integration);
// Output API URL
new cdk.CfnOutput(this, 'ApiUrl', {
value: api.url,
description: 'API Gateway URL',
});
}
}
Environment Configuration¶
Environment-Specific Configuration¶
// lib/config/dev.ts
export const devConfig = {
env: {
account: '111111111111',
region: 'us-east-1',
},
tags: {
Environment: 'development',
CostCenter: 'Engineering',
},
vpc: {
maxAzs: 2,
natGateways: 1,
},
rds: {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
multiAz: false,
},
};
// lib/config/prod.ts
export const prodConfig = {
env: {
account: '222222222222',
region: 'us-east-1',
},
tags: {
Environment: 'production',
CostCenter: 'Engineering',
},
vpc: {
maxAzs: 3,
natGateways: 3,
},
rds: {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.LARGE),
multiAz: true,
},
};
// bin/my-cdk-app.ts
import { devConfig } from '../lib/config/dev';
import { prodConfig } from '../lib/config/prod';
const environment = process.env.ENVIRONMENT || 'dev';
const config = environment === 'prod' ? prodConfig : devConfig;
new MyStack(app, `MyStack-${environment}`, {
env: config.env,
tags: config.tags,
config,
});
Testing¶
Unit Tests with Jest¶
import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { MyStack } from '../lib/my-cdk-app-stack';
describe('MyStack', () => {
test('S3 Bucket Created', () => {
const app = new cdk.App();
const stack = new MyStack(app, 'TestStack');
const template = Template.fromStack(stack);
template.resourceCountIs('AWS::S3::Bucket', 1);
template.hasResourceProperties('AWS::S3::Bucket', {
VersioningConfiguration: {
Status: 'Enabled',
},
});
});
test('Bucket has encryption enabled', () => {
const app = new cdk.App();
const stack = new MyStack(app, 'TestStack');
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::S3::Bucket', {
BucketEncryption: {
ServerSideEncryptionConfiguration: [
{
ServerSideEncryptionByDefault: {
SSEAlgorithm: 'AES256',
},
},
],
},
});
});
});
CDK Commands¶
Common Commands¶
## Initialize new CDK project
cdk init app --language typescript
## Install dependencies
npm install
## Synthesize CloudFormation template
cdk synth
## Diff against deployed stack
cdk diff
## Deploy stack
cdk deploy
## Deploy all stacks
cdk deploy --all
## Deploy with approval
cdk deploy --require-approval never
## Destroy stack
cdk destroy
## List all stacks
cdk list
## View documentation
cdk doctor
## Bootstrap environment (first time only)
cdk bootstrap aws://ACCOUNT-NUMBER/REGION
Best Practices¶
Use Stack Outputs¶
new cdk.CfnOutput(this, 'BucketName', {
value: bucket.bucketName,
description: 'The name of the S3 bucket',
exportName: 'MyBucketName',
});
Tagging¶
cdk.Tags.of(this).add('Project', 'MyProject');
cdk.Tags.of(this).add('Owner', 'Platform Team');
cdk.Tags.of(myResource).add('Critical', 'true');
Removal Policies¶
// Development - destroy resources
new s3.Bucket(this, 'DevBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
// Production - retain resources
new s3.Bucket(this, 'ProdBucket', {
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
// Snapshot before deletion
new rds.DatabaseInstance(this, 'Database', {
removalPolicy: cdk.RemovalPolicy.SNAPSHOT,
});
Security Best Practices¶
Never Hardcode Secrets¶
Avoid storing sensitive data in CDK code:
// Bad - Hardcoded secrets
const database = new rds.DatabaseInstance(this, 'Database', {
masterUsername: 'admin',
masterPassword: 'MySecretPassword123', // ❌ Exposed in code!
});
// Good - Use Secrets Manager
const dbSecret = new secretsmanager.Secret(this, 'DBSecret', {
generateSecretString: {
secretStringTemplate: JSON.stringify({ username: 'admin' }),
generateStringKey: 'password',
excludePunctuation: true,
},
});
const database = new rds.DatabaseInstance(this, 'Database', {
credentials: rds.Credentials.fromSecret(dbSecret), // ✅ From Secrets Manager
});
// Good - Reference existing secrets
const apiKey = secretsmanager.Secret.fromSecretNameV2(
this,
'ApiKey',
'prod/api-key'
);
Key Points:
- Never hardcode credentials in CDK code
- Use AWS Secrets Manager for secrets
- Reference secrets, don't embed them
- Rotate secrets automatically
- Use IAM roles instead of access keys
- Audit secret access
Encryption at Rest and in Transit¶
Enable encryption for all data:
// Good - S3 encryption
const bucket = new s3.Bucket(this, 'Bucket', {
encryption: s3.BucketEncryption.S3_MANAGED, // ✅ Server-side encryption
// Or use KMS for more control:
// encryption: s3.BucketEncryption.KMS,
// encryptionKey: myKmsKey,
enforceSSL: true, // ✅ Require HTTPS
});
// Good - RDS encryption
const database = new rds.DatabaseInstance(this, 'Database', {
storageEncrypted: true, // ✅ Encrypt at rest
storageEncryptionKey: myKmsKey, // Use customer-managed key
});
// Good - EBS encryption
const instance = new ec2.Instance(this, 'Instance', {
blockDevices: [{
deviceName: '/dev/xvda',
volume: ec2.BlockDeviceVolume.ebs(30, {
encrypted: true, // ✅ Encrypted EBS
kmsKey: myKmsKey,
}),
}],
});
Key Points:
- Enable encryption for all storage (S3, EBS, RDS)
- Use KMS for key management
- Enforce SSL/TLS for data in transit
- Enable encryption by default
- Use customer-managed keys for sensitive data
- Implement key rotation
IAM Least Privilege¶
Grant minimum required permissions:
// Bad - Overly permissive IAM
const role = new iam.Role(this, 'Role', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess'), // ❌ Too permissive!
],
});
// Good - Least privilege
const role = new iam.Role(this, 'Role', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
});
// Grant specific permissions only
bucket.grantRead(role); // ✅ Only read access to specific bucket
// Or create custom policy
role.addToPolicy(new iam.PolicyStatement({
actions: ['s3:GetObject'],
resources: [`${bucket.bucketArn}/public/*`], // ✅ Specific resources only
}));
Key Points:
- Never use
AdministratorAccessor*permissions - Use high-level grant methods (
grantRead,grantWrite) - Specify exact resources in policies
- Use condition keys to further restrict access
- Regular IAM access review
- Implement permission boundaries
Network Security¶
Implement proper network isolation:
// Good - VPC with proper segmentation
const vpc = new ec2.Vpc(this, 'VPC', {
maxAzs: 3,
subnetConfiguration: [
{
cidrMask: 24,
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: 'Private',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
{
cidrMask: 28,
name: 'Isolated',
subnetType: ec2.SubnetType.PRIVATE_ISOLATED, // ✅ No internet access
},
],
});
// Good - Restrictive security groups
const dbSecurityGroup = new ec2.SecurityGroup(this, 'DatabaseSG', {
vpc,
description: 'Security group for RDS database',
allowAllOutbound: false, // ✅ Explicit egress rules
});
// Only allow from application security group
dbSecurityGroup.addIngressRule(
appSecurityGroup,
ec2.Port.tcp(5432),
'Allow PostgreSQL from app'
);
// Good - NACLs for additional security
const nacl = new ec2.NetworkAcl(this, 'NACL', {
vpc,
subnetSelection: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
});
nacl.addEntry('DenySSH', {
cidr: ec2.AclCidr.anyIpv4(),
ruleNumber: 100,
traffic: ec2.AclTraffic.tcpPort(22),
direction: ec2.TrafficDirection.INGRESS,
ruleAction: ec2.Action.DENY, // ✅ Deny SSH
});
Key Points:
- Use private subnets for sensitive resources
- Create isolated subnets for databases
- Implement restrictive security groups
- Use NACLs for additional layer
- Enable VPC Flow Logs
- Implement AWS PrivateLink for AWS services
Resource Deletion Protection¶
Protect critical resources from accidental deletion:
// Good - Deletion protection for databases
const database = new rds.DatabaseInstance(this, 'Database', {
deletionProtection: true, // ✅ Cannot be deleted
removalPolicy: cdk.RemovalPolicy.RETAIN, // ✅ Keep on stack deletion
backupRetention: cdk.Duration.days(30),
});
// Good - S3 bucket protection
const bucket = new s3.Bucket(this, 'DataBucket', {
removalPolicy: cdk.RemovalPolicy.RETAIN, // ✅ Keep bucket
versioned: true, // Enable versioning
lifecycleRules: [{
noncurrentVersionExpiration: cdk.Duration.days(90),
}],
});
// Good - Prevent accidental destruction
cdk.Aspects.of(this).add(new cdk.Tag('Environment', 'production'));
Key Points:
- Use
RemovalPolicy.RETAINfor production resources - Enable deletion protection on databases
- Enable versioning on S3 buckets
- Require manual approval for destructive changes
- Use stack policies to prevent updates
- Implement backup and recovery procedures
Logging and Monitoring¶
Enable comprehensive logging:
// Good - CloudTrail for audit logging
new cloudtrail.Trail(this, 'Trail', {
isMultiRegionTrail: true,
includeGlobalServiceEvents: true,
managementEvents: cloudtrail.ReadWriteType.ALL,
});
// Good - VPC Flow Logs
vpc.addFlowLog('FlowLog', {
destination: ec2.FlowLogDestination.toCloudWatchLogs(),
trafficType: ec2.FlowLogTrafficType.ALL,
});
// Good - S3 bucket logging
const logBucket = new s3.Bucket(this, 'LogBucket', {
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});
bucket.enableEventBridgeNotification();
bucket.addEventNotification(
s3.EventType.OBJECT_CREATED,
new s3n.SnsDestination(topic),
{ prefix: 'sensitive/' }
);
// Good - Lambda function logging
const fn = new lambda.Function(this, 'Function', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
logRetention: logs.RetentionDays.ONE_MONTH,
insightsVersion: lambda.LambdaInsightsVersion.VERSION_1_0_229_0, // ✅ CloudWatch Insights
});
Key Points:
- Enable CloudTrail for all accounts
- Configure VPC Flow Logs
- Enable S3 bucket logging and access logs
- Use CloudWatch Logs for application logs
- Set appropriate log retention
- Monitor and alert on suspicious activity
Security Scanning¶
Implement security scanning in CI/CD:
// package.json - Add security scanning
{
"scripts": {
"test": "jest",
"cdk": "cdk",
"security": "npm audit && cdk-nag",
"synth": "cdk synth",
"deploy": "npm run security && cdk deploy"
},
"devDependencies": {
"cdk-nag": "^2.0.0"
}
}
// bin/app.ts - Add CDK Nag for compliance
import { AwsSolutionsChecks } from 'cdk-nag';
import { Aspects } from 'aws-cdk-lib';
const app = new cdk.App();
// Add security checks
Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));
new MyStack(app, 'MyStack');
Key Points:
- Use cdk-nag for security scanning
- Run security checks in CI/CD pipeline
- Scan for common misconfigurations
- Implement compliance frameworks (CIS, PCI-DSS)
- Regular dependency audits (
npm audit) - Update CDK and dependencies regularly
Common Pitfalls¶
Circular Stack Dependencies¶
Issue: Creating circular dependencies between stacks causes CDK synthesis to fail.
Example:
## Bad - Circular dependency
export class VpcStack extends cdk.Stack {
public readonly vpcId: string;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const vpc = new ec2.Vpc(this, 'VPC');
this.vpcId = vpc.vpcId;
// ❌ Referencing AppStack creates circular dependency!
const appStack = new AppStack(this, 'App', { vpcId: this.vpcId });
}
}
export class AppStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: { vpcId: string }) {
super(scope, id);
// Uses VPC from VpcStack
}
}
Solution: Pass values between stacks or use cross-stack references properly.
## Good - Pass values between stacks
const vpcStack = new VpcStack(app, 'VpcStack');
const appStack = new AppStack(app, 'AppStack', {
vpcId: vpcStack.vpcId // ✅ One-way dependency
});
## Good - Export and import values
export class VpcStack extends cdk.Stack {
public readonly vpc: ec2.IVpc;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
this.vpc = new ec2.Vpc(this, 'VPC');
}
}
export class AppStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: { vpc: ec2.IVpc }) {
super(scope, id);
// ✅ Uses passed VPC interface
}
}
Key Points:
- Stacks should have unidirectional dependencies
- Pass resources as props between stacks
- Use CloudFormation exports for cross-stack references
- Check
cdk synthoutput for dependency issues
Missing Removal Policy¶
Issue: Stateful resources without removal policy prevent stack deletion.
Example:
## Bad - No removal policy
new s3.Bucket(this, 'DataBucket', {
versioned: true
// ❌ No removalPolicy! Stack deletion will fail if bucket has objects
});
new dynamodb.Table(this, 'Users', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }
// ❌ Default is RETAIN, stack won't delete table
});
Solution: Explicitly set removal policies for stateful resources.
## Good - Explicit removal policies
new s3.Bucket(this, 'DataBucket', {
versioned: true,
removalPolicy: cdk.RemovalPolicy.RETAIN, # ✅ Explicit: keep on stack delete
autoDeleteObjects: false // Don't auto-delete in production
});
new s3.Bucket(this, 'TempBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY, # ✅ Delete with stack (dev/test)
autoDeleteObjects: true
});
new dynamodb.Table(this, 'Users', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
removalPolicy: cdk.RemovalPolicy.SNAPSHOT # ✅ Create snapshot before delete
});
Key Points:
RETAIN: Keep resource on stack deletion (production default)DESTROY: Delete resource with stack (dev/test)SNAPSHOT: Create snapshot before deletion (databases)- Set
autoDeleteObjects: truefor S3 DESTROY
Physical Name Hardcoding¶
Issue: Hardcoded physical names prevent multiple stack instances and updates.
Example:
## Bad - Hardcoded physical names
new s3.Bucket(this, 'Bucket', {
bucketName: 'my-app-bucket' // ❌ Can't create multiple instances!
});
new dynamodb.Table(this, 'Table', {
tableName: 'users', // ❌ Prevents parallel deployments
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }
});
Solution: Let CDK generate names or use tokens.
## Good - Generated names
new s3.Bucket(this, 'Bucket', {
// ✅ CDK generates unique name
});
## Good - Name with stack and environment
new s3.Bucket(this, 'Bucket', {
bucketName: `my-app-${this.stackName}-${this.account}`.toLowerCase() # ✅ Unique
});
## Good - Use physical name only when required
const table = new dynamodb.Table(this, 'Table', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }
// ✅ No hardcoded name - allows multiple environments
});
// Reference generated name
new cdk.CfnOutput(this, 'TableName', {
value: table.tableName // ✅ Output the generated name
});
Key Points:
- Avoid hardcoded physical names when possible
- Let CDK generate unique names
- Use stack name, account, region for uniqueness
- Only hardcode when required by external systems
Environment-Agnostic Stack Anti-Pattern¶
Issue: Not specifying env makes stack environment-agnostic, limiting CDK features.
Example:
## Bad - Environment-agnostic stack
const stack = new MyStack(app, 'MyStack'); // ❌ No env specified
// ❌ Can't use environment-specific features:
// - ec2.Vpc.fromLookup() fails
// - Hosted zones lookup fails
// - Cross-region/account references don't work
Solution: Explicitly specify environment.
## Good - Explicit environment
const stack = new MyStack(app, 'MyStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT, # ✅ From AWS CLI config
region: process.env.CDK_DEFAULT_REGION
}
});
## Good - Multiple environments
const devStack = new MyStack(app, 'DevStack', {
env: { account: '123456789012', region: 'us-east-1' } # ✅ Explicit dev
});
const prodStack = new MyStack(app, 'ProdStack', {
env: { account: '987654321098', region: 'us-west-2' } # ✅ Explicit prod
});
Key Points:
- Always specify
envfor production stacks - Use environment variables for flexibility
- Environment-agnostic stacks can't use lookups
- Required for cross-account/region references
Construct ID Conflicts¶
Issue: Duplicate construct IDs within same scope cause synthesis errors.
Example:
## Bad - Duplicate IDs
new s3.Bucket(this, 'Bucket'); // First bucket
new s3.Bucket(this, 'Bucket'); // ❌ Same ID! Error
const vpc = new ec2.Vpc(this, 'VPC');
const sg = new ec2.SecurityGroup(this, 'VPC', { vpc }); // ❌ Conflicts with VPC ID!
Solution: Use unique, descriptive construct IDs.
## Good - Unique IDs
new s3.Bucket(this, 'DataBucket'); # ✅ Descriptive
new s3.Bucket(this, 'LogsBucket'); # ✅ Unique
new s3.Bucket(this, 'AssetsBucket'); # ✅ Clear purpose
const vpc = new ec2.Vpc(this, 'ApplicationVPC');
const sg = new ec2.SecurityGroup(this, 'WebSecurityGroup', { vpc }); # ✅ Different IDs
Key Points:
- Construct IDs must be unique within parent scope
- Use descriptive IDs (not generic names like 'Resource')
- IDs form part of CloudFormation logical IDs
- Changing IDs causes resource replacement
Anti-Patterns¶
❌ Avoid: Hardcoded Values¶
// Bad
new s3.Bucket(this, 'Bucket', {
bucketName: 'my-hardcoded-bucket-name',
});
// Good - Let CDK generate names
new s3.Bucket(this, 'Bucket');
// Good - Use configuration
new s3.Bucket(this, 'Bucket', {
bucketName: props.bucketName,
});
❌ Avoid: Not Using TypeScript Strict Mode¶
// tsconfig.json - Enable strict mode
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true
}
}
❌ Avoid: No Tests¶
// Always write tests for your stacks
describe('MyStack', () => {
test('Stack creates resources', () => {
const app = new cdk.App();
const stack = new MyStack(app, 'TestStack');
const template = Template.fromStack(stack);
template.resourceCountIs('AWS::S3::Bucket', 1);
});
});
❌ Avoid: Not Using Constructs¶
// Bad - Using low-level L1 constructs directly
new s3.CfnBucket(this, 'Bucket', {
bucketName: 'my-bucket',
versioningConfiguration: {
status: 'Enabled'
}
});
// Good - Use high-level L2 constructs
new s3.Bucket(this, 'Bucket', {
versioned: true,
encryption: s3.BucketEncryption.S3_MANAGED,
removalPolicy: cdk.RemovalPolicy.RETAIN
});
❌ Avoid: Not Specifying Removal Policy¶
// Bad - Default removal policy (may delete production data)
new dynamodb.Table(this, 'Table', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }
// No removalPolicy - uses default
});
// Good - Explicit removal policy
new dynamodb.Table(this, 'Table', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
removalPolicy: cdk.RemovalPolicy.RETAIN // ✅ Protect production data
});
❌ Avoid: Single Stack for Everything¶
// Bad - Everything in one massive stack
export class MonolithStack extends cdk.Stack {
constructor(scope: Construct, id: string) {
super(scope, id);
// VPC
// Database
// Lambda functions
// API Gateway
// S3 buckets
// ... 500 lines of resources
}
}
// Good - Separate stacks by concern
export class NetworkStack extends cdk.Stack {
public readonly vpc: ec2.Vpc;
constructor(scope: Construct, id: string) {
super(scope, id);
this.vpc = new ec2.Vpc(this, 'VPC');
}
}
export class DatabaseStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: { vpc: ec2.Vpc }) {
super(scope, id);
new rds.DatabaseInstance(this, 'Database', {
vpc: props.vpc
});
}
}
❌ Avoid: Not Using Environment Variables¶
// Bad - Environment-specific values in code
const app = new cdk.App();
new MyStack(app, 'ProdStack', {
env: { account: '123456789012', region: 'us-east-1' } // ❌ Hardcoded
});
// Good - Use environment variables
const app = new cdk.App();
new MyStack(app, 'Stack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION
}
});
Tool Configuration¶
cdk.json¶
{
"app": "npx ts-node --prefer-ts-exts bin/app.ts",
"watch": {
"include": ["**"],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": ["aws", "aws-cn"],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false
}
}
tsconfig.json¶
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["es2020"],
"declaration": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": false,
"inlineSourceMap": true,
"inlineSources": true,
"experimentalDecorators": true,
"strictPropertyInitialization": false,
"typeRoots": ["./node_modules/@types"],
"resolveJsonModule": true,
"esModuleInterop": true
},
"exclude": ["node_modules", "cdk.out"]
}
package.json Scripts¶
{
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"cdk": "cdk",
"synth": "cdk synth",
"deploy": "cdk deploy",
"deploy:all": "cdk deploy --all",
"diff": "cdk diff",
"destroy": "cdk destroy",
"bootstrap": "cdk bootstrap",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
"format": "prettier --write \"**/*.ts\"",
"format:check": "prettier --check \"**/*.ts\"",
"validate": "npm run lint && npm run format:check && npm run test"
},
"devDependencies": {
"@types/jest": "^29.5.0",
"@types/node": "^20.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"aws-cdk": "^2.100.0",
"eslint": "^8.50.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.0",
"typescript": "^5.2.0"
},
"dependencies": {
"aws-cdk-lib": "^2.100.0",
"constructs": "^10.0.0",
"source-map-support": "^0.5.21"
}
}
ESLint Configuration¶
// .eslintrc.js
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
project: './tsconfig.json',
},
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
],
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/explicit-function-return-type': 'warn',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_' },
],
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/await-thenable': 'error',
},
ignorePatterns: ['*.js', '*.d.ts', 'node_modules/', 'cdk.out/'],
};
Jest Configuration¶
// jest.config.js
module.exports = {
testEnvironment: 'node',
roots: ['<rootDir>/test'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
collectCoverageFrom: [
'lib/**/*.ts',
'!lib/**/*.d.ts',
'!lib/**/*.test.ts',
],
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70,
},
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};
Prettier Configuration¶
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "always"
}
Pre-commit Hooks¶
## .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.1.0
hooks:
- id: prettier
types_or: [javascript, jsx, ts, tsx, json]
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.56.0
hooks:
- id: eslint
files: \.[jt]sx?$
types: [file]
additional_dependencies:
- eslint@8.56.0
- '@typescript-eslint/eslint-plugin@6.21.0'
- '@typescript-eslint/parser@6.21.0'
VS Code Settings¶
{
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
"cdk.path": "node_modules/.bin/cdk"
}
Makefile¶
## Makefile
.PHONY: install build test deploy clean
install:
npm install
build:
npm run build
test:
npm run test
test-coverage:
npm run test:coverage
lint:
npm run lint
lint-fix:
npm run lint:fix
format:
npm run format
format-check:
npm run format:check
validate: lint format-check test
@echo "✓ All validations passed"
synth:
npm run synth
diff:
npm run diff
deploy:
npm run deploy
deploy-all:
npm run deploy:all
destroy:
npm run destroy
clean:
rm -rf node_modules cdk.out coverage .nyc_output
rm -f *.js *.d.ts
bootstrap:
npm run bootstrap
References¶
Official Documentation¶
Additional Resources¶
Status: Active