AWS CDK: Cloud Development Kit for Infrastructure

Define AWS infrastructure using TypeScript, Python, or other programming languages with the AWS Cloud Development Kit, compiling to CloudFormation templates.

published: reading time: 12 min read

AWS CDK: Cloud Development Kit for Infrastructure

The AWS Cloud Development Kit (CDK) brings infrastructure as code into the world of familiar programming languages. You write TypeScript, Python, Java, or C# code, and CDK synthesizes it into CloudFormation templates that AWS then deploys. This gives you the full power of object-oriented programming, complete with inheritance, composition, and testability, while still getting the reliable deployment mechanism that CloudFormation provides.

CDK sits on top of CloudFormation, so you get all the benefits of CloudFormation’s rollback behavior, change sets, and drift detection. The difference is that you write application code instead of JSON or YAML templates.

When to Use / When Not to Use

When CDK makes sense

CDK is the right choice for AWS-heavy teams that want infrastructure code with the full expressiveness of a programming language. If your team writes TypeScript or Python for applications, CDK lets them use the same language and tooling for infrastructure without switching contexts.

Use CDK when your infrastructure benefits from object-oriented design. If you have shared patterns across dozens of services—like a standard VPC setup, a consistent ECS service definition, or a common API Gateway configuration—CDK’s inheritance and composition model shines. You define a base construct once and specialize it across teams.

CDK also excels when you want aggressive testing of infrastructure. The Template.fromStack() assertion API lets you write property checks that run against synthesized templates in milliseconds. If your team already writes Jest or Pytest tests, CDK testing feels natural.

CDK Pipelines, which define the CI/CD pipeline itself as CDK code, is particularly well-integrated. The pipeline updates itself when infrastructure code changes, which is harder to achieve with external CI tools.

When to stick with Terraform or Pulumi

If your infrastructure spans multiple clouds, CDK locks you into AWS. Terraform’s provider model handles AWS, Azure, GCP, and dozens of other platforms from the same configuration language. For multi-cloud strategies, CDK is not the answer.

If your team is ops-focused rather than software-engineering-focused, raw CloudFormation or Terraform HCL may be faster to learn than TypeScript. CDK adds a layer of abstraction on top of CloudFormation—the additional expressiveness is only valuable if you need it.

CDK apps can also be slower to synthesize for very large infrastructure graphs. If you have thousands of resources, the TypeScript-to-CloudFormation compilation step can become noticeable. Terraform’s plan phase is often faster for massive state files.

CDK Overview and Installation

CDK applications are organized into stacks, which map to CloudFormation stacks. Within each stack, you define constructs, which are the building blocks of your infrastructure. Constructs represent AWS resources like an S3 bucket or an ECS service, but they can also represent patterns, like a load-balanced web service that combines an autoscaling group, a load balancer, and a security group.

# Install the CDK CLI
npm install -g aws-cdk

# Initialize a new CDK project
cdk init --language typescript

# Install AWS modules
npm install @aws-cdk/aws-ec2 @aws-cdk/aws-ecs @aws-cdk/aws-ecs-patterns

# List stacks in the app
cdk list

# Synthesize to CloudFormation
cdk synth

The cdk synth command converts your TypeScript code into a CloudFormation template. You can review the generated JSON or YAML before deploying anything. This step also validates that your constructs are internally consistent and that you have permission to create the resources you are requesting.

Construct Model and L3 Constructs

CDK uses a three-level construct model. L1 constructs are direct mappings to CloudFormation resources. You use them when you need precise control over every CloudFormation property. L2 constructs have sensible defaults and convenience methods. L3 constructs, also called patterns, are opinionated solutions for common use cases.

import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as ecsPatterns from "aws-cdk-lib/aws-ecs-patterns";

class WebServiceStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Create a VPC
    const vpc = new ec2.Vpc(this, "WebServiceVPC", {
      maxAzs: 2,
      natGateways: 1,
      subnetConfiguration: [
        { cidrMask: 24, name: "Public", subnetType: ec2.SubnetType.PUBLIC },
        {
          cidrMask: 24,
          name: "Private",
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        },
      ],
    });

    // Create an ECS cluster
    const cluster = new ecs.Cluster(this, "WebServiceCluster", {
      vpc,
      clusterName: "web-service-cluster",
    });

    // Use L3 pattern for a load-balanced Fargate service
    const loadBalancedService =
      new ecsPatterns.ApplicationLoadBalancedFargateService(
        this,
        "WebService",
        {
          cluster,
          memoryLimitMiB: 1024,
          cpu: 512,
          desiredCount: 2,
          taskImageOptions: {
            image: ecs.ContainerImage.fromRegistry("nginx:latest"),
            environment: {
              NODE_ENV: "production",
            },
          },
        },
      );

    // Output the load balancer URL
    new cdk.CfnOutput(this, "LoadBalancerURL", {
      value: loadBalancedService.loadBalancer.loadBalancerDnsName,
    });
  }
}

const app = new cdk.App();
new WebServiceStack(app, "WebServiceStack");

The ApplicationLoadBalancedFargateService pattern creates everything needed for a production-ready service: a Fargate task definition, an ECS service, an Application Load Balancer, a target group, a security group, and IAM roles. One line of configuration replaces dozens of resource definitions.

Synthesizing to CloudFormation

When you run cdk synth, CDK reads your TypeScript files, resolves any runtime values, and generates CloudFormation templates in the cdk.out directory. These templates are what CloudFormation actually deploys.

# Synthesize to CloudFormation template
cdk synth MyStack

# Compare current state with deployed state
cdk diff MyStack

# Deploy the stack
cdk deploy MyStack

# Destroy the stack
cdk destroy MyStack

The cdk diff command is particularly useful. It compares your synthesized template against the currently deployed stack and shows you exactly what will change. This is a safe way to review changes before applying them, especially in production environments.

CDK also supports context values and runtime values that cannot be known at synthesis time, like the current AWS account ID or the latest AMI ID. These get resolved during deployment rather than synthesis.

CDK Pipelines for CI/CD

CDK pipelines let you define your deployment pipeline as code. A pipeline stack contains the pipeline itself, along with stages that represent deployment environments like staging and production.

import * as codepipeline from "aws-cdk-lib/aws-codepipeline";
import * as codepipelineActions from "aws-cdk-lib/aws-codepipeline-actions";
import * as codebuild from "aws-cdk-lib/aws-codebuild";

class PipelineStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const sourceArtifact = new codepipeline.Artifact();
    const cloudAssemblyArtifact = new codepipeline.Artifact();

    const pipeline = new codepipeline.Pipeline(this, "DeploymentPipeline", {
      pipelineName: "my-app-pipeline",
      synth: new codepipelineActions.CodeBuildAction({
        actionName: "Synth",
        input: sourceArtifact,
        outputs: [cloudAssemblyArtifact],
        buildSpec: codebuild.BuildSpec.fromObject({
          version: "0.2",
          phases: {
            install: { commands: ["npm ci"] },
            build: { commands: ["npx cdk synth"] },
          },
          artifacts: {
            "discard-paths": "yes",
            "base-directory": "cdk.out",
            files: ["**/*"],
          },
        }),
      }),
    });

    // Add source stage
    pipeline.addStage({
      stageName: "Source",
      actions: [
        new codepipelineActions.GitHubSourceAction({
          actionName: "GitHub",
          owner: "my-org",
          repo: "my-app",
          branch: "main",
          output: sourceArtifact,
          oauthToken: cdk.SecretValue.secretsManager("github-token"),
        }),
      ],
    });

    // Add stage for deploying to production
    pipeline.addStage({
      stageName: "Prod",
      actions: [
        new codepipelineActions.CloudFormationCreateUpdateStackAction({
          actionName: "Deploy",
          stackName: "MyAppStack",
          template: cloudAssemblyArtifact,
          adminPermissions: true,
        }),
      ],
    });
  }
}

The pipeline itself is deployed via CloudFormation. When your code changes, the pipeline automatically detects the change, synthesizes the template, and deploys to each stage in sequence.

Testing CDK Applications

CDK integrates with standard testing frameworks. You can write snapshot tests that verify your synthesized templates match expected output, or you can write assertion tests that validate specific resource properties.

import { App } from "aws-cdk-lib";
import { Template } from "aws-cdk-lib/assertions";
import { MyStack } from "../lib/my-stack";

test("VPC is created with correct CIDR", () => {
  const app = new App();
  const stack = new MyStack(app, "TestStack");

  const template = Template.fromStack(stack);

  template.hasResourceProperties("AWS::EC2::VPC", {
    CidrBlock: "10.0.0.0/16",
    EnableDnsHostnames: true,
    EnableDnsSupport: true,
  });
});

test("S3 bucket has versioning enabled", () => {
  const app = new App();
  const stack = new MyStack(app, "TestStack");

  const template = Template.fromStack(stack);

  template.hasResourceProperties("AWS::S3::Bucket", {
    VersioningConfiguration: {
      Status: "Enabled",
    },
  });
});

The Template.fromStack() method parses a synthesized CloudFormation template and provides methods to assert the presence and properties of resources. These tests run quickly because they work against the synthesized template without actually deploying anything.

CDK vs Terraform vs Pulumi Comparison

AspectCDKTerraformPulumi
LanguageTypeScript, Python, Java, C#HCLTypeScript, Python, Go, C#
EcosystemAWS-focusedMulti-cloud, massiveMulti-cloud, growing
StateCloudFormation handles itSelf-managed backendsManaged or self-managed
TestingJest/Pytest assertionsPlan validationUnit tests with language frameworks
Learning curveModerate (requires programming)Gentle for opsModerate (requires programming)
Synthesize stepGenerates JSON/YAML templatesNative planNative plan

Production Failure Scenarios

Common CDK Failures

FailureImpactMitigation
Missing synthesizer permissionsStack creation fails mid-deployBootstrap AWS account with CDK toolkit first
Circular construct dependenciesSynth hangs or crashesAudit dependency graph before deploying
Context values missing in CIDifferent synthesis in CI vs localEnsure consistent AWS account/region context
Deep inheritance chainsCDK app slow to synthesizeUse composition over inheritance for constructs
Unpinned ConstructLib versionsDifferent resource versions across environmentsPin all ConstructLib versions in package.json

Bootstrap Failure Recovery

flowchart TD
    A[cdk deploy fails] --> B{Bootstrap needed?}
    B -->|Yes| C[Run cdk bootstrap]
    B -->|No| D[Check IAM permissions]
    C --> E[Creates S3 bucket, IAM roles]
    E --> F[Retry deploy]
    D --> G[Add required IAM permissions]
    G --> F

Best Practices for Team Workflows

Organize CDK projects around the concept of team ownership. Each team maintains their own constructs and stacks, publishing them as libraries that other teams consume. This creates clear boundaries and prevents merge conflicts in monorepo setups.

Use the CDK Bootcamp pattern for constructing cloud-native applications. Stacks should represent deployment environments, constructs should represent components, and the assembly of constructs into stacks should reflect how the application is actually deployed.

Always run cdk synth in your CI pipeline before deploying. This catches syntax errors, type errors, and logical errors before they reach any environment.

Observability Hooks

What to monitor:

  • CDK app synthesis duration — slow synthesis usually means deep dependency chains or overly complex constructs
  • CloudFormation stack drift between deployments
  • Deploy frequency per stack — deploying too often suggests missing abstraction
  • Failed CDK deployments and error types
  • CloudFormation template size — approach the 51,200-byte limit and synthesis starts failing
# List all stacks and their status
cdk list | while read stack; do
  echo "=== $stack ==="
  aws cloudformation describe-stacks --stack-name "$stack" \
    --query 'Stacks[0].[StackStatus,LastUpdatedTimestamp]'
done

# Check for drift between synthesized and deployed state
cdk diff --no-color

# Monitor CloudFormation stack events during deployment
aws cloudformation describe-stack-events \
  --stack-name MyAppStack \
  --query 'StackEvents[?ResourceStatus==`CREATE_FAILED`]'

# CDK metadata and version info
cdk --version
npm list aws-cdk-lib

Common Pitfalls / Anti-Patterns

Using L1 constructs everywhere

L1 constructs map directly to CloudFormation resources. Using them exclusively gives you precise control but loses the productivity benefits of L2 and L3 constructs. Reserve L1 for properties that higher-level constructs do not expose.

Ignoring the CDK context cache

CDK caches things like availability zones and AMI IDs in .cdk.json. This cache can become stale after region outages or when you add a new region. Clear it with cdk context --clear when you suspect stale data.

Deploying from local machines instead of CI

When individual engineers deploy from their machines, you lose audit trails and introduce inconsistency. CDK Pipelines or CI-based deployments ensure every deployment is version-controlled and logged.

Modifying synthesized templates directly

CDK always regenerates templates from source during cdk synth. If you hand-edit a synthesized template in cdk.out, your changes vanish on the next synthesis. All modifications belong in the source code.

Using cdk deploy --require-approval never in production

Skipping manual approval checks in production defeats the purpose of reviewable change sets. Use cdk diff to review changes before deploying, even when not requiring approval at the CLI level.

Quick Recap

Key Takeaways

  • CDK synthesizes TypeScript, Python, Java, or C# into CloudFormation templates
  • L2 and L3 constructs provide sensible defaults—only drop to L1 when needed
  • cdk synth validates before deploy—always run it in CI
  • CDK Pipelines define the deployment pipeline as code
  • Template assertions enable fast infrastructure testing without deploying
  • For multi-cloud: stick with Terraform or Pulumi; CDK is AWS-only

CDK Health Checklist

# Synthesize and validate
cdk synth

# Diff against deployed stack
cdk diff

# Bootstrap account (one-time setup)
cdk bootstrap aws://123456789012/us-east-1

# Deploy with approval
cdk deploy --require-approval always

# List all stacks
cdk list

# Destroy stacks
cdk destroy

# Run tests
npm test

# Check for missing context values
cdk context --list

CDK Architecture Flow

flowchart TD
    A[CDK Code .ts/.py] --> B[cdk synth]
    B --> C[CloudFormation Template]
    C --> D[CloudFormation Stack]
    D --> E[AWS Resources]
    E --> F[cdk diff]
    F -->|Drift detected| G[Update via CDK]
    F -->|No drift| H[No action needed]

Conclusion

CDK brings programming language expressiveness to AWS infrastructure while leveraging CloudFormation’s battle-tested deployment engine. The construct model provides sensible defaults while allowing full customization when needed. CDK pipelines bring CI/CD best practices to infrastructure deployments, and integrated testing ensures your infrastructure code does what you intend.

For securing your AWS infrastructure, see Cloud Security for IAM policies, encryption, and VPC configuration. For monitoring CDK-deployed resources and pipeline health, see Observability Engineering.

If your team lives primarily in the AWS ecosystem and already writes TypeScript or Python, CDK is worth serious consideration. The productivity gains from IDE support, type checking, and software engineering practices like testing and composition are significant compared to raw CloudFormation templates.

For more on AWS services, see our post on Cost Optimization which covers AWS cost management strategies.

Category

Related Posts

AWS Core Services for DevOps: EC2, ECS, EKS, S3, Lambda

Navigate essential AWS services for DevOps workloads—compute (EC2, ECS, EKS), storage (S3), serverless (Lambda), and foundational networking.

#aws #cloud #devops

IaC Module Design: Reusable and Composable Infrastructure

Design Terraform modules that are reusable, composable, and maintainable—versioning, documentation, and publish patterns for infrastructure building blocks.

#terraform #iac #modules

IaC State Management: Remote Backends and Team Collaboration

Manage Terraform/OpenTofu state securely with remote backends, state locking, and strategies for team collaboration without state conflicts.

#terraform #iac #state-management