Back to Blog

Infrastructure as Code with AWS CDK: A Complete Guide

September 14, 2025
10 min read
AWSCDKInfrastructureTypeScriptDevOps

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:

Terminal
# 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 install

Project Structure

A typical CDK project structure looks like this:

Project Structure
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            # Dependencies

Building 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:

lib/web-app-stack.ts
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:

lib/database-construct.ts
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:

cdk.json
{
  "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:

test/web-app-stack.test.ts
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:

Deploy Commands
# 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 never

Key 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!