AWS CDK (Cloud Development Kit) has transformed how we manage infrastructure by bringing the power of programming languages to infrastructure as code. Instead of writing YAML or JSON templates, you can now define your AWS resources using familiar languages like TypeScript, Python, or Java.
In this comprehensive guide, we'll explore how to leverage AWS CDK to build scalable, maintainable infrastructure with TypeScript, covering everything from basic concepts to advanced patterns used in production.
Why Choose AWS CDK?
Beyond CloudFormation Templates
While CloudFormation templates are powerful, they can become unwieldy as your infrastructure grows. CDK addresses these challenges:
- Type Safety: Catch errors at compile time, not deployment time
- Code Reuse: Create reusable constructs and share them across projects
- IDE Support: IntelliSense, refactoring, and debugging capabilities
- Familiar Syntax: Use loops, conditions, and functions naturally
- Testing: Unit test your infrastructure code
Getting Started with CDK
Installation and Setup
First, install the AWS CDK CLI and initialize a new TypeScript project:
# Install CDK CLI globally
npm install -g aws-cdk
# Create a new CDK project
mkdir my-cdk-project && cd my-cdk-project
cdk init app --language typescript
# Install dependencies
npm installProject Structure
A typical CDK project structure looks like this:
my-cdk-project/
├── bin/
│ └── my-cdk-project.ts # Entry point
├── lib/
│ └── my-cdk-project-stack.ts # Stack definition
├── test/
│ └── my-cdk-project.test.ts # Unit tests
├── cdk.json # CDK configuration
└── package.json # DependenciesBuilding Your First Stack
Creating a Simple Web Application Stack
Let's build a stack that includes an S3 bucket for static hosting, CloudFront distribution, and a Lambda function:
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import { Construct } from 'constructs';
export class WebAppStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// S3 bucket for static website hosting
const websiteBucket = new s3.Bucket(this, 'WebsiteBucket', {
websiteIndexDocument: 'index.html',
websiteErrorDocument: 'error.html',
publicReadAccess: true,
removalPolicy: cdk.RemovalPolicy.DESTROY, // Only for dev/test
});
// Lambda function for API
const apiFunction = new lambda.Function(this, 'ApiFunction', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
environment: {
BUCKET_NAME: websiteBucket.bucketName,
},
});
// API Gateway
const api = new apigateway.LambdaRestApi(this, 'Api', {
handler: apiFunction,
proxy: false,
});
// Add API resources
const items = api.root.addResource('items');
items.addMethod('GET');
items.addMethod('POST');
// CloudFront distribution
const distribution = new cloudfront.CloudFrontWebDistribution(this, 'Distribution', {
originConfigs: [
{
s3OriginSource: {
s3BucketSource: websiteBucket,
},
behaviors: [{ isDefaultBehavior: true }],
},
],
});
// Output the CloudFront URL
new cdk.CfnOutput(this, 'DistributionUrl', {
value: distribution.distributionDomainName,
description: 'CloudFront Distribution URL',
});
}
}Advanced CDK Patterns
Creating Reusable Constructs
One of CDK's most powerful features is the ability to create reusable constructs. Here's an example of a database construct:
import * as cdk from 'aws-cdk-lib';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';
export interface DatabaseConstructProps {
vpc: ec2.Vpc;
instanceClass?: ec2.InstanceClass;
instanceSize?: ec2.InstanceSize;
databaseName: string;
}
export class DatabaseConstruct extends Construct {
public readonly database: rds.DatabaseInstance;
public readonly secret: rds.DatabaseSecret;
constructor(scope: Construct, id: string, props: DatabaseConstructProps) {
super(scope, id);
// Create database secret
this.secret = new rds.DatabaseSecret(this, 'DatabaseSecret', {
username: 'admin',
description: 'Database credentials',
});
// Create database subnet group
const subnetGroup = new rds.SubnetGroup(this, 'DatabaseSubnetGroup', {
vpc: props.vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
description: 'Subnet group for database',
});
// Create database instance
this.database = new rds.DatabaseInstance(this, 'Database', {
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_15_3,
}),
instanceType: ec2.InstanceType.of(
props.instanceClass ?? ec2.InstanceClass.T3,
props.instanceSize ?? ec2.InstanceSize.MICRO
),
vpc: props.vpc,
subnetGroup,
credentials: rds.Credentials.fromSecret(this.secret),
databaseName: props.databaseName,
backupRetention: cdk.Duration.days(7),
deletionProtection: false, // Set to true for production
});
}
}Environment-Specific Configuration
Use CDK context and environment variables to manage different deployment environments:
{
"app": "npx ts-node --prefer-ts-exts bin/my-app.ts",
"context": {
"environments": {
"dev": {
"instanceClass": "t3.micro",
"certificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/dev-cert",
"domainName": "dev.myapp.com"
},
"prod": {
"instanceClass": "t3.medium",
"certificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/prod-cert",
"domainName": "myapp.com"
}
}
}
}Testing Your Infrastructure
Unit Testing with Jest
CDK allows you to unit test your infrastructure code. Here's an example test:
import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { WebAppStack } from '../lib/web-app-stack';
test('Web App Stack creates S3 bucket', () => {
const app = new cdk.App();
const stack = new WebAppStack(app, 'TestStack');
const template = Template.fromStack(stack);
// Assert S3 bucket is created
template.hasResourceProperties('AWS::S3::Bucket', {
WebsiteConfiguration: {
IndexDocument: 'index.html',
ErrorDocument: 'error.html',
},
});
});
test('Web App Stack creates Lambda function', () => {
const app = new cdk.App();
const stack = new WebAppStack(app, 'TestStack');
const template = Template.fromStack(stack);
// Assert Lambda function is created
template.hasResourceProperties('AWS::Lambda::Function', {
Runtime: 'nodejs18.x',
Handler: 'index.handler',
});
});Deployment and Best Practices
Deployment Pipeline
Set up a proper CI/CD pipeline for your infrastructure:
# Synthesize CloudFormation templates
cdk synth
# Compare deployed stack with current state
cdk diff
# Deploy to development environment
cdk deploy --context environment=dev
# Deploy to production environment
cdk deploy --context environment=prod --require-approval neverKey Best Practices
- Use meaningful construct names: Make them descriptive and consistent
- Implement proper tagging: Tag resources for cost tracking and management
- Leverage aspects: Apply cross-cutting concerns like security policies
- Version your constructs: Use semantic versioning for reusable constructs
- Use feature flags: Control feature rollouts through context variables
- Implement proper error handling: Handle deployment failures gracefully
Conclusion
AWS CDK represents a significant step forward in infrastructure as code, bringing software engineering best practices to infrastructure management. By leveraging TypeScript's type safety, IDE support, and familiar programming constructs, teams can build more maintainable and scalable infrastructure.
Start small with a simple stack, gradually adopt advanced patterns like custom constructs and testing, and always follow security best practices. The investment in learning CDK will pay dividends in improved development velocity and infrastructure reliability.
Have you migrated from CloudFormation to CDK? What challenges did you face and how did CDK help solve them? Share your experience!