AWS infrastructure as code framework that allows defining cloud resources using programming languages like TypeScript, Python, or Java, generating CloudFormation.
AWS CDK (Cloud Development Kit) is an infrastructure as code framework that allows defining AWS resources using familiar programming languages like TypeScript, Python, Java, C#, and Go. Unlike CloudFormation which uses YAML/JSON, CDK leverages programming language capabilities to create reusable abstractions, apply conditional logic, and perform compile-time validations.
The framework works by synthesizing code into CloudFormation templates that are then deployed using AWS's native engine. This architecture maintains CloudFormation's robustness and features (automatic rollbacks, drift detection, stack dependencies) while gaining the benefits of imperative programming.
CDK introduces the concept of "constructs" — reusable components that encapsulate one or more AWS resources with sensible default configurations. These constructs range from direct CloudFormation resource mappings to complete architectural patterns that configure multiple services with integrated best practices.
CDK organizes constructs into three abstraction levels that determine the degree of control versus convenience:
| Level | Name | Description | Example | Use case |
|---|---|---|---|---|
| L1 | CFN Resources | 1:1 mapping with CloudFormation | CfnBucket, CfnFunction | Granular control, new resources |
| L2 | AWS Constructs | Abstractions with defaults | s3.Bucket, lambda.Function | Productive development |
| L3 | Patterns | Complete architectures | LambdaRestApi, ApplicationLoadBalancedFargateService | Common patterns |
L2 constructs are the sweet spot for most cases — they provide sensible defaults (encryption enabled, minimal IAM policies) while maintaining configuration flexibility. L3 constructs implement complete architectural patterns but may be too rigid for specific cases.
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import { Construct } from 'constructs';
export class UserServiceStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// DynamoDB table with production configuration
const userTable = new dynamodb.Table(this, 'UserTable', {
tableName: 'users',
partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
encryption: dynamodb.TableEncryption.AWS_MANAGED,
pointInTimeRecovery: true,
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
// Lambda function with optimized configuration
const userHandler = new lambda.Function(this, 'UserHandler', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
environment: {
TABLE_NAME: userTable.tableName,
},
timeout: cdk.Duration.seconds(30),
memorySize: 512,
tracing: lambda.Tracing.ACTIVE,
});
// Granular permissions for DynamoDB
userTable.grantReadWriteData(userHandler);
// API Gateway with production configuration
const api = new apigateway.RestApi(this, 'UserApi', {
restApiName: 'User Service API',
description: 'API for user management',
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS,
allowMethods: apigateway.Cors.ALL_METHODS,
},
deployOptions: {
stageName: 'prod',
throttlingRateLimit: 1000,
throttlingBurstLimit: 2000,
loggingLevel: apigateway.MethodLoggingLevel.INFO,
},
});
const users = api.root.addResource('users');
users.addMethod('GET', new apigateway.LambdaIntegration(userHandler));
users.addMethod('POST', new apigateway.LambdaIntegration(userHandler));
const userById = users.addResource('{userId}');
userById.addMethod('GET', new apigateway.LambdaIntegration(userHandler));
userById.addMethod('PUT', new apigateway.LambdaIntegration(userHandler));
userById.addMethod('DELETE', new apigateway.LambdaIntegration(userHandler));
// Outputs for integration with other stacks
new cdk.CfnOutput(this, 'ApiUrl', {
value: api.url,
description: 'URL of the User API',
});
new cdk.CfnOutput(this, 'TableName', {
value: userTable.tableName,
description: 'Name of the DynamoDB table',
});
}
}| Aspect | AWS CDK | Terraform |
|---|---|---|
| Languages | TypeScript, Python, Java, C#, Go | HCL (HashiCorp Configuration Language) |
| Providers | AWS only (native) | Multi-cloud (AWS, Azure, GCP, etc.) |
| State | Managed by CloudFormation | Local/remote state file |
| Abstractions | Native L1/L2/L3 constructs | Community modules |
| Testing | Native language unit tests | Terratest, kitchen-terraform |
| IDE Support | Full IntelliSense | HCL extensions |
| Learning curve | Familiar to developers | Domain-specific language |
| Rollbacks | Automatic via CloudFormation | Manual or via CI/CD |
| Drift detection | Native in CloudFormation | Terraform plan |
| Ecosystem | Constructs Hub, CDK Patterns | Terraform Registry |
For teams considering migrating from Terraform to CDK:
cdk import to import existing resourcesCDK enables real unit testing using language frameworks:
import { Template, Match } from 'aws-cdk-lib/assertions';
import { UserServiceStack } from '../lib/user-service-stack';
import { App } from 'aws-cdk-lib';
test('DynamoDB table created with correct configuration', () => {
const app = new App();
const stack = new UserServiceStack(app, 'TestStack');
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::DynamoDB::Table', {
BillingMode: 'PAY_PER_REQUEST',
SSESpecification: {
SSEEnabled: true,
},
PointInTimeRecoverySpecification: {
PointInTimeRecoveryEnabled: true,
},
});
});
test('Lambda has correct environment variables', () => {
const app = new App();
const stack = new UserServiceStack(app, 'TestStack');
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::Lambda::Function', {
Environment: {
Variables: {
TABLE_NAME: { Ref: Match.stringLikeRegexp('UserTable') },
},
},
});
});// Database stack
export class DatabaseStack extends cdk.Stack {
public readonly userTable: dynamodb.Table;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
this.userTable = new dynamodb.Table(this, 'UserTable', {
// configuration...
});
}
}
// API stack that consumes the database
export class ApiStack extends cdk.Stack {
constructor(scope: Construct, id: string,
databaseStack: DatabaseStack,
props?: cdk.StackProps) {
super(scope, id, props);
const handler = new lambda.Function(this, 'Handler', {
environment: {
TABLE_NAME: databaseStack.userTable.tableName,
},
// configuration...
});
databaseStack.userTable.grantReadWriteData(handler);
}
}export interface ApiLambdaProps {
tableName: string;
timeout?: cdk.Duration;
memorySize?: number;
}
export class ApiLambda extends Construct {
public readonly function: lambda.Function;
public readonly api: apigateway.RestApi;
constructor(scope: Construct, id: string, props: ApiLambdaProps) {
super(scope, id);
this.function = new lambda.Function(this, 'Function', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
environment: { TABLE_NAME: props.tableName },
timeout: props.timeout ?? cdk.Duration.seconds(30),
memorySize: props.memorySize ?? 512,
});
this.api = new apigateway.LambdaRestApi(this, 'Api', {
handler: this.function,
proxy: false,
});
}
}CDK represents a paradigmatic shift in infrastructure as code by eliminating the artificial barrier between application code and infrastructure. For senior engineering teams, this means being able to apply the same development practices — unit testing, refactoring, abstractions, composition — to infrastructure definition.
The ability to create reusable constructs enables establishing organizational "golden paths" that encapsulate security, observability, and cost best practices. A custom L3 construct can guarantee that all APIs include structured logging, CloudWatch metrics, and minimal IAM policies without requiring expert knowledge from every developer.
The native testing model eliminates the need for external tools like Terratest, enabling real TDD for infrastructure. This is especially valuable in organizations that prioritize code quality and test coverage. IDE integration provides autocompletion, real-time error detection, and automatic refactoring — capabilities impossible with DSLs like HCL or YAML.
Practice of defining and managing infrastructure through versioned configuration files instead of manual processes. Foundation of modern operations automation.
Typed superset of JavaScript adding optional static types, improving developer productivity, error detection, and code maintainability.
AWS native service for defining and provisioning infrastructure as code using YAML or JSON templates, with state management and automatic rollback.