Introduction#
After deploying dozens of CDK applications to production, I’ve learned that while CDK makes infrastructure deployment easier, it doesn’t automatically make it better. Here are the essential practices that separate hobby projects from production-ready infrastructure.
Project Structure & Organization#
1. Separate Stacks by Lifecycle#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // ❌ Monolithic stack
class EverythingStack extends Stack {
// VPC, databases, applications, monitoring all mixed together
}
// ✅ Separated by lifecycle and concerns
class NetworkStack extends Stack {
// VPC, subnets, NAT gateways
}
class DatabaseStack extends Stack {
// RDS, DynamoDB, ElastiCache
}
class ApplicationStack extends Stack {
// Lambda, ECS, API Gateway
}
|
2. Use Constructs for Reusability#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Create reusable constructs for common patterns
export class MonitoredLambda extends Construct {
public readonly function: lambda.Function;
constructor(scope: Construct, id: string, props: MonitoredLambdaProps) {
super(scope, id);
this.function = new lambda.Function(this, 'Function', {
...props,
environment: {
...props.environment,
LOG_LEVEL: props.logLevel || 'INFO'
}
});
// Add monitoring automatically
this.addCloudWatchAlarms();
this.addXRayTracing();
}
}
|
Configuration Management#
3. Environment-Specific Configuration#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // config/environments.ts
export interface EnvironmentConfig {
account: string;
region: string;
domainName: string;
certificateArn?: string;
enableBackups: boolean;
logRetentionDays: number;
}
export const environments: Record<string, EnvironmentConfig> = {
dev: {
account: '111111111111',
region: 'us-east-1',
domainName: 'dev.example.com',
enableBackups: false,
logRetentionDays: 7
},
prod: {
account: '222222222222',
region: 'us-east-1',
domainName: 'example.com',
certificateArn: 'arn:aws:acm:...',
enableBackups: true,
logRetentionDays: 30
}
};
|
4. Use CDK Context for Runtime Configuration#
1
2
3
4
5
6
7
| // In your stack
const environment = this.node.tryGetContext('environment') || 'dev';
const config = environments[environment];
if (!config) {
throw new Error(`Unknown environment: ${environment}`);
}
|
Security Best Practices#
5. Principle of Least Privilege#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // ❌ Overly broad permissions
const role = new iam.Role(this, 'LambdaRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('PowerUserAccess')
]
});
// ✅ Specific permissions only
const role = new iam.Role(this, 'LambdaRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
inlinePolicies: {
DynamoDBAccess: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['dynamodb:GetItem', 'dynamodb:PutItem'],
resources: [table.tableArn]
})
]
})
}
});
|
6. Secure Secrets Management#
1
2
3
4
5
6
7
8
9
| // ✅ Use AWS Secrets Manager or Parameter Store
const dbPassword = secretsmanager.Secret.fromSecretNameV2(
this, 'DBPassword', 'prod/database/password'
);
const database = new rds.DatabaseInstance(this, 'Database', {
credentials: rds.Credentials.fromSecret(dbPassword),
// ... other props
});
|
Testing Strategies#
7. Unit Tests for Business Logic#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // tests/stacks/application-stack.test.ts
import { Template } from 'aws-cdk-lib/assertions';
import { ApplicationStack } from '../lib/application-stack';
test('Lambda function has correct environment variables', () => {
const app = new cdk.App();
const stack = new ApplicationStack(app, 'TestStack', {
environment: 'test'
});
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::Lambda::Function', {
Environment: {
Variables: {
LOG_LEVEL: 'INFO',
NODE_ENV: 'production'
}
}
});
});
|
8. Integration Tests#
1
2
3
4
5
6
7
8
9
10
| // tests/integration/api.test.ts
describe('API Integration Tests', () => {
test('API returns correct response', async () => {
const response = await fetch(`${process.env.API_URL}/health`);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.status).toBe('healthy');
});
});
|
Deployment Patterns#
9. Blue/Green Deployments for Critical Services#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| export class BlueGreenApiStack extends Stack {
constructor(scope: Construct, id: string, props: BlueGreenApiProps) {
super(scope, id, props);
// Create two identical environments
const blueEnvironment = this.createApiEnvironment('Blue', props);
const greenEnvironment = this.createApiEnvironment('Green', props);
// Route 53 weighted routing for traffic shifting
const hostedZone = route53.HostedZone.fromLookup(this, 'Zone', {
domainName: props.domainName
});
new route53.ARecord(this, 'BlueRecord', {
zone: hostedZone,
recordName: 'api',
target: route53.RecordTarget.fromAlias(
new targets.ApiGatewayDomain(blueEnvironment.domainName)
),
setIdentifier: 'blue',
weight: props.blueWeight
});
}
}
|
10. Rollback Strategy#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // Always tag deployments for easy rollback
new cdk.CfnOutput(this, 'DeploymentVersion', {
value: process.env.GITHUB_SHA || 'local-deployment',
description: 'Git commit SHA for this deployment'
});
// Use stack policies to prevent accidental deletion
const stackPolicy = {
Statement: [{
Effect: 'Deny',
Principal: '*',
Action: 'Update:Delete',
Resource: '*',
Condition: {
StringEquals: {
'ResourceType': [
'AWS::RDS::DBInstance',
'AWS::S3::Bucket'
]
}
}
}]
};
|
Monitoring & Observability#
11. Comprehensive Monitoring#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| export class MonitoringStack extends Stack {
constructor(scope: Construct, id: string, props: MonitoringProps) {
super(scope, id, props);
// Create dashboard
const dashboard = new cloudwatch.Dashboard(this, 'ApplicationDashboard', {
dashboardName: `${props.applicationName}-${props.environment}`
});
// Add widgets for key metrics
dashboard.addWidgets(
new cloudwatch.GraphWidget({
title: 'API Response Times',
left: [props.apiGateway.metricLatency()],
right: [props.apiGateway.metricCount()]
})
);
// Create alarms
new cloudwatch.Alarm(this, 'HighErrorRate', {
metric: props.apiGateway.metricServerError(),
threshold: 10,
evaluationPeriods: 2,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING
});
}
}
|
12. Resource Tagging Strategy#
1
2
3
4
5
6
7
8
9
10
11
12
| // Consistent tagging across all resources
const commonTags = {
Project: 'MyApplication',
Environment: props.environment,
Owner: 'platform-team',
CostCenter: 'engineering',
ManagedBy: 'CDK'
};
// Apply to all constructs in the stack
cdk.Tags.of(this).add('Project', commonTags.Project);
cdk.Tags.of(this).add('Environment', commonTags.Environment);
|
13. Cost Optimization#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Use appropriate instance sizes based on environment
const instanceType = props.environment === 'prod'
? ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.LARGE)
: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO);
// Enable deletion protection only in production
const deletionProtection = props.environment === 'prod';
const database = new rds.DatabaseInstance(this, 'Database', {
instanceType,
deletionProtection,
backupRetention: props.environment === 'prod'
? cdk.Duration.days(30)
: cdk.Duration.days(1)
});
|
Key Takeaways#
- Structure matters: Organize stacks by lifecycle and responsibility
- Security first: Always apply least privilege and secure secrets properly
- Test everything: Unit tests for logic, integration tests for behavior
- Monitor proactively: Set up comprehensive monitoring from day one
- Plan for failure: Implement proper rollback and disaster recovery
- Optimize costs: Right-size resources and use appropriate retention policies
- Document decisions: Use clear naming and comprehensive comments
Conclusion#
CDK is a powerful tool, but like any tool, it requires discipline and best practices to use effectively. These patterns have served me well across multiple production deployments and will help you build more reliable, secure, and maintainable infrastructure.
Remember: infrastructure code is still code. Apply the same rigor you would to application development.
What CDK patterns have you found most valuable? Share your experiences on LinkedIn or GitHub.