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: 32 min read author: GeekWorkBench updated: June 17, 2026
Quick Summary

AWS CDK brings real programming language expressiveness to infrastructure by compiling TypeScript, Python, or Java into CloudFormation templates. This guide walks through the construct model (L1, L2, L3), CDK Pipelines for self-updating CI/CD, and the Template.fromStack() assertion API for testing infrastructure. You will learn when CDK beats Terraform, how to avoid deep inheritance chains that slow synthesis, and how to handle context values that differ between local and CI environments. By the end you will know whether CDK fits your team and how to use it without creating a maintenance nightmare.

AWS CDK: Cloud Development Kit for Infrastructure

The AWS Cloud Development Kit (CDK) brings infrastructure as code into familiar programming languages. You write TypeScript, Python, Java, or C# code, and CDK turns it into CloudFormation templates that AWS then deploys. This gives you object-oriented programming — inheritance, composition, testability — while still leaning on CloudFormation’s deployment engine.

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.

Introduction

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. Shared patterns across dozens of services — a standard VPC setup, a consistent ECS service definition, a common API Gateway configuration — are where CDK’s inheritance and composition model shines. You define a base construct once and specialize it across teams.

CDK also shines 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 fits naturally.

CDK Pipelines, which defines the CI/CD pipeline itself as CDK code, is particularly well-integrated. The pipeline updates itself when infrastructure code changes, which is harder to pull off 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 on top of CloudFormation — the additional expressiveness only pays off if you need it.

CDK apps can also be slower to synthesize for very large infrastructure graphs. Thousands of resources mean 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 — 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 — 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

Trade-off Analysis

CDK vs Alternatives

ConsiderationCDKTerraformPulumi
Multi-cloud supportAWS-onlyFull multi-cloudFull multi-cloud
Language expressivenessFull OOP (inheritance, composition)HCL limited to declarativeGeneral-purpose languages
State managementCloudFormation handles automaticallySelf-managed or remote backendsSelf-managed or cloud backends
Ecosystem sizeGrowing, AWS-focusedMassive, mature provider networkGrowing, multi-cloud
Learning curveModerate (requires TypeScript/Python)Gentle for ops engineersModerate (requires programming)
Synthesis speedSlower for large graphs (thousands of resources)Faster plan phase for massive stateSimilar to Terraform
IDE/Type supportFirst-class TypeScript, auto-completeLimited HCL supportFull language tooling
TestingJest/Pytest assertions against synthesized JSONPlan validation, external toolsNative unit testing with language frameworks
Pipeline integrationCDK Pipelines (self-updating)External CI/CDExternal CI/CD
Breakage riskConstruct library updates can change behaviorProvider version changesSame as Terraform

When CDK Wins

CDK excels when your infrastructure has structural repetition that benefits from abstraction. The classic scenario is a platform team that defines standard patterns — a shared VPC with a standard subnet strategy, an ECS service definition with consistent logging and monitoring, an API Gateway with standard authorizer configuration — and distributes these as internal construct libraries. Application teams then instantiate the base construct with environment-specific parameters rather than rebuilding from scratch.

// Platform team publishes this as a library
class StandardVPC extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props: StandardVPCProps) {
    super(scope, id, props);

    const vpc = new ec2.Vpc(this, "StandardVPC", {
      maxAzs: props.maxAzs,
      natGateways: props.natGateways,
      subnetConfiguration: [
        { cidrMask: 24, name: "Public", subnetType: ec2.SubnetType.PUBLIC },
        {
          cidrMask: 24,
          name: "Private",
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        },
      ],
    });

    // Standard tagging applied to all resources
    cdk.Tags.of(vpc).add("Environment", props.environment);
    cdk.Tags.of(vpc).add("Owner", props.team);
  }
}

// Application teams use it without rebuilding the pattern
new StandardVPC(app, "PaymentServiceVPC", {
  maxAzs: 3,
  natGateways: 2,
  environment: "production",
  team: "payments",
});

When the platform team updates the base construct — adding a new tag requirement, enabling VPC Flow Logs, adjusting the NAT gateway configuration — all fifty consuming services pick up the change on their next synthesis. With Terraform, the same update propagation requires module versioning and registry publication, which adds friction that CDK teams avoid.

The second win is teams already writing TypeScript or Python. The infrastructure learning curve shrinks to CDK-specific concepts — constructs, stacks, App — rather than learning a new language on top of infrastructure concepts. Engineers stay in their IDE, use their existing test runners, and review infrastructure changes with the same pull request workflow used for application code. A Jest config that runs React component tests also runs VPC construct tests, without modification.

CDK Pipelines is a third strong case. Defining the deployment pipeline as CDK code means the pipeline updates itself when infrastructure changes. There is no separate pipeline configuration file to drift out of sync. When the pipeline stack changes, the pipeline updates itself before deploying the next stage — which is difficult to achieve with external CI tools.

When CDK Loses

If your infrastructure spans multiple clouds, CDK is not an option. CDK synthesizes to CloudFormation — a service that only exists in AWS. Terraform’s provider model handles AWS, Azure, GCP, and dozens of other platforms from the same configuration language. A team standardizing on Terraform for multi-cloud can define a network module once and apply it to an AWS VPC in one environment and a GCP VPC in another. CDK cannot do this. The investment in CDK constructs does not transfer.

The second scenario is at large scale. CDK synthesis compiles TypeScript to CloudFormation JSON. For an infrastructure graph of 5000+ resources — a reasonable size for a large organization’s AWS footprint — the compilation step can take several minutes. Terraform’s plan phase handles state files of similar complexity faster because it does not compile a language to an intermediate format; it evaluates a declarative configuration directly against the state graph.

Slow synthesis extends deploy times and increases the temptation to skip CI and deploy locally to save time. Teams with large CDK apps often split them into multiple CDK apps with separate deploy pipelines, which adds operational complexity that Terraform avoids.

The third scenario is ops-focused teams who prefer infrastructure tooling that matches their mental model. CDK requires programming-language thinking: loops, conditionals, inheritance chains, type systems. An ops engineer who thinks in desired state, resource declarations, and dependency graphs may find CDK adds a translation layer between their intent and the infrastructure. Terraform HCL is closer to the metal — a declarative configuration format rather than a general-purpose language — and some teams consider that a feature.

One hybrid worth considering: CDK for application infrastructure (ECS services, RDS databases, API Gateways that live close to application code) and Terraform for foundational infrastructure (VPCs, shared IAM roles, account-level policies). Cross-stack references between CDK and Terraform are awkward, but this lets each tool handle what it does best.

Production Failure Scenarios

Common CDK Failures

CDK failures usually surface during synthesis or deployment. They differ from plain CloudFormation errors because they emerge from the translation between your TypeScript or Python code and the CloudFormation template that CDK generates. Once you know the common categories, debugging goes much faster.

These five cover most of the CDK-specific problems teams hit in practice. The table breaks each one down by symptom, what goes wrong operationally, and what you can do to fix it. It’s useful when something breaks or as a preventive checklist during code review.

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

CDK bootstrap is a one-time setup that gets an AWS account and region ready for CDK deployments. It provisions an S3 bucket where CDK stores CloudFormation templates during deployment, plus IAM roles that CDK assumes at runtime. Bootstrap errors typically mean either the account has never been bootstrapped, or someone deleted or modified the bootstrap resources.

The most common scenario is running cdk deploy in a new account without bootstrapping first. CloudFormation cannot upload the template because there is no S3 bucket. The error message will mention a missing toolkit bucket or insufficient permissions to create IAM roles. Run cdk bootstrap with the target account and region, wait for the CloudFormation stack to finish, then retry.

A less common but more destructive scenario is when someone deletes the bootstrap S3 bucket or modifies the bootstrap IAM roles. CDK deploys fail silently in some cases or produce confusing validation errors in others. The fix is to bootstrap again, which recreates the bucket and roles. Note that bootstrapping again does not affect existing deployed stacks — it only prepares the account for future deploys.

The flowchart below shows how to diagnose a failed CDK deploy. Start at the top and follow the branches based on what you see in the error output.

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

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]

Common Pitfalls / Anti-Patterns

Using L1 constructs everywhere

L1 constructs map directly to CloudFormation resources — every property the underlying API supports is accessible, but none of it is pre-wired with defaults. The result is verbose code that requires manual updates whenever CloudFormation resource schemas change.

Treat L1 as the escape hatch layer, not the primary abstraction. Start with L2 (Bucket, Vpc, Cluster) or L3 (ApplicationLoadBalancedFargateService, LoadBalancedFargateService) and drop to L1 only when a specific CloudFormation property has no L2 equivalent.

The L2 Bucket construct automatically enables versioning, server-side encryption, and a lifecycle rule that aborts incomplete multipart uploads after 7 days. Writing that from scratch with L1 requires defining all of those explicitly. But if you need to set a custom ObjectLambdaConfiguration on an S3 bucket — a property L2 does not expose — you access the underlying resource via the escape hatch:

import * as s3 from "aws-cdk-lib/aws-s3";

const bucket = new s3.Bucket(this, "DataBucket", {});

// Access the L1 CfnBucket to set a property L2 doesn't expose
const cfnBucket = bucket.node.defaultChild as s3.CfnBucket;
cfnBucket.objectLambdaConfiguration = {
  AwsLambdaTransformation: [
    {
      FunctionArn: myLambda.functionArn,
      TransformationType: "GZIP",
    },
  ],
};

This works, but your code becomes tightly coupled to the CloudFormation schema. When AWS adds new fields to the underlying resource, L2/L3 constructs absorb those changes automatically. L1 code breaks silently unless you actively update it. Teams using L1 as their primary construct level spend time on maintenance that L2 would handle for free.

Default to L2, use L3 for standard patterns, reach for L1 only when L2 has no other option.

Ignoring the CDK context cache

CDK queries AWS at synthesis time for values that cannot be known statically — availability zones in your account, the latest Amazon Linux AMI ID, SSM parameter values, and VPC CIDR allocations. Rather than making those API calls on every cdk synth, CDK caches results in cdk.context.json. This makes synthesis fast and deterministic on a given machine, but it causes problems when the cached data diverges across machines or goes stale after AWS infrastructure changes.

When context values go stale

After a regional outage, AWS may add new availability zones or retire others. If CDK cached the AZ list before the change, your synthesized template references AZs that no longer exist in the account. cdk synth still succeeds because the cache has not been invalidated, but cdk deploy fails with a validation error about an unavailable AZ. The error message does not say “your cache is stale,” so the root cause is non-obvious.

AMI IDs change with every patch release. If you use aws-ec2.AmLinuxImage() without pinning a specific ID, CDK stores the resolved AMI ID on first synthesis and reuses it indefinitely. Six months later your instances are running an outdated kernel with known security vulnerabilities, and the deployment succeeded silently.

CI vs local divergence

The most damaging scenario is a CI pipeline running on a different machine than developers. If an engineer runs cdk synth locally and the cache is populated with account-specific AZs, those values get embedded in the synthesized template artifact. The CI runner, with an empty cache on a fresh build agent, queries different AZs and synthesizes a different template. The result is a deployment that differs from what was reviewed — cdk diff shows changes even though no infrastructure code changed.

The fix is to exclude cdk.out/ from version control and run cdk synth in CI from a clean state. If you need consistent context values across machines, commit cdk.context.json to the repo — this enforces consistency at the cost of slower first-run synthesis after any context change.

Managing the cache
# Clear all cached context values (forces fresh AWS API calls)
cdk context --clear

# List current cached values with their origins
cdk context

# Remove a specific cached value by key
cdk context --reset /aws/service/ecs/optimized-ami/amazon-linux-2

For CI pipelines, pass context values explicitly at synthesis time to make builds reproducible:

cdk synth --context aws:availability-zones:account=123456789012:region=us-east-1

Slower synthesis, but the output is deterministic. The alternative — committing cdk.context.json to share values across machines — works until a context value legitimately changes, at which point every engineer and CI runner needs to update the file simultaneously.

Deploying from local machines instead of CI

When engineers run cdk deploy from their workstations, infrastructure changes happen without any codified pipeline. The deployment is tied to whatever version of the code is checked out locally, with no guarantee it matches what was reviewed in a pull request. In regulated environments this is a compliance problem. In teams with multiple engineers making concurrent changes it is an operational risk.

The concrete failure mode: Engineer A deploys version 1.2.3 of the stack from their laptop. Engineer B merges a pull request that bumps the version to 1.2.4 and also changes the VPC CIDR. Engineer A’s next deployment from the older local checkout applies only part of that change — the version bump but not the CIDR change, because their local state is out of sync with main. The result is a partial deployment that is hard to diagnose because the CI pipeline has no record of Engineer A’s manual run.

Local deploys also bypass the review gate that cdk diff in a pull request provides. A CI/CD pipeline that requires cdk diff output as a pull request comment forces asynchronous review of infrastructure changes. Local deploys require no such step.

CDK Pipelines solve this by expressing the deployment pipeline as CDK code. The pipeline is deployed via CloudFormation and updates itself when the pipeline definition changes. Engineers push to GitHub, the pipeline triggers, synthesis runs in CodeBuild, and the resulting CloudFormation template deploys to each stage in sequence. The pipeline definition — stages, deploy order, manual approval gates — lives in the repository alongside the infrastructure code and follows the same review process.

If CDK Pipelines are too heavy, a simpler approach is a CI-only deploy policy: cdk deploy runs only in CI, never locally, and CI triggers only on merges to protected branches. Engineer workstations get read-only access — cdk diff and cdk synth are allowed, but cdk deploy is blocked by IAM policy. You get audit trails and consistency without the self-updating pipeline overhead.

Modifying synthesized templates directly

Every cdk synth run regenerates CloudFormation templates from the TypeScript source. The cdk.out/ directory is not a deployment artifact you maintain — it is a generated output that gets discarded and recreated on every run. Hand-editing a template in cdk.out/ is a temporary local change that disappears the moment any team member runs cdk synth.

Teams get caught out when they make what seems like a reasonable operational adjustment — adding a tag to a synthesized template, changing a retry policy, pinning a CIDR — and the change silently vanishes after the next synthesis. Worse, if that modified template was deployed before the next synthesis, the deployed stack has a configuration that does not match any version of the source code. Your infrastructure no longer has a reproducible source of truth.

Any infrastructure change belongs in the CDK source. If you need to add a tag to every resource, add it to the construct that creates the resource, not to the synthesized template. If a CloudFormation property is missing from the CDK construct API, use the escape hatch — set it on the underlying CfnResource via .node.defaultChild — not by editing the JSON output.

If you need to understand what a template contains without running synthesis, use cdkMetadata annotations or query the CDK app programmatically. If you need to patch a generated template for a one-off scenario while a proper fix is pending, document the patch in the source code with a tracking issue number, not as a change to cdk.out/.

Modifying synthesized output and re-deploying is how infrastructure drifts happen. CDK’s position is unambiguous: source is authoritative, cdk.out/ is ephemeral.

Using cdk deploy --require-approval never in production

The --require-approval never flag tells CDK to skip the interactive approval prompt that normally appears before cdk deploy applies changes to a stack. In development this is fine — fast iteration is the goal. In production it removes the last human review gate before infrastructure changes go live.

CloudFormation change sets are the mechanism that makes infrastructure deployments reviewable. Running cdk diff shows the difference between your synthesized template and the currently deployed stack. Running cdk deploy creates a change set — a structured preview of what will change — and pauses for approval unless --require-approval never is set. The change set shows which resources will be created, modified, or deleted, along with their properties. Without this step, you deploy blind.

The --require-approval never flag does not eliminate change sets — it skips the human review step. CloudFormation still creates and evaluates the change set internally. The flag’s effect is to automatically proceed past the review rather than waiting for confirmation. This is useful in CI where there is no human to press Enter. It is harmful in production where human review is the only safeguard against unintended changes.

Even with --require-approval never, the change set still exists and can be reviewed afterward via the CloudFormation console or aws cloudformation describe-change-set. By then the deployment has already succeeded or failed — the review is too late to prevent mistakes.

For production, a better approach is to add a manual approval stage to CDK Pipelines, which pauses after the change set is generated and before CloudFormation executes it. Reviewers get a web console link to the change set and an approval action to proceed or reject. If you are not using CDK Pipelines, the equivalent is to remove --require-approval never from your production deploy command and add a CloudFormation change set review step in your CI pipeline before the deploy step.

The risk of skipping approval scales with how often your infrastructure changes and how many engineers own it. A team deploying once a week with five engineers reviewing pull requests has low exposure. A team with ten engineers making daily deploys has high exposure to someone merging a change that breaks production without a human catching it.

Interview Questions

1. What is the relationship between CDK and CloudFormation?

Expected answer points:

  • CDK synthesizes application code into CloudFormation templates
  • CloudFormation handles deployment, rollback, and state management
  • CDK adds object-oriented abstraction on top of CloudFormation's JSON/YAML
  • All CloudFormation features (change sets, drift detection, rollback) work with CDK
2. Explain the three levels of CDK constructs (L1, L2, L3).

Expected answer points:

  • L1: direct CloudFormation resource mappings, full control, verbose
  • L2: sensible defaults and convenience methods, preferred for most use
  • L3: opinionated patterns (like ApplicationLoadBalancedFargateService), replace dozens of resources
3. How do you handle missing context values in CI/CD environments?

Expected answer points:

  • Context values (AZs, AMI IDs) resolved at synthesis time
  • CI environments may lack account/region context that local has
  • Ensure consistent context via CDK bootstrapping and environment variables
  • Use `cdk context --clear` when stale data causes issues
4. What is the CDK Bootcamp pattern?

Expected answer points:

  • Stacks represent deployment environments (dev, staging, prod)
  • Constructs represent components (VPC, ECS, Database)
  • Assembly of constructs into stacks reflects actual deployment topology
  • Creates clear ownership boundaries between teams
5. How do you test CDK applications?

Expected answer points:

  • Snapshot tests verify synthesized templates match expected output
  • Assertion tests validate specific resource properties via `Template.fromStack()`
  • Tests run against synthesized JSON without actual deployment
  • Integration with Jest/Pytest for teams already using those frameworks
6. Why is `cdk synth` run in CI before deployment?

Expected answer points:

  • Catches syntax errors, type errors, and logical errors before any environment
  • Validates construct internal consistency
  • Checks IAM permissions for requested resources
  • Generates CloudFormation template for review via `cdk diff`
7. How do CDK Pipelines differ from external CI tools?

Expected answer points:

  • Pipeline definition is CDK code that updates itself when infrastructure changes
  • No external CI configuration required—the pipeline IS infrastructure
  • Pipeline deployment via CloudFormation ensures self-consistency
  • External CI tools require separate pipeline configuration that can drift
8. What causes circular construct dependencies and how do you avoid them?

Expected answer points:

  • Circular dependencies cause synth to hang or crash
  • Avoid by using composition over inheritance for construct relationships
  • Audit dependency graph before deploying large infrastructure
  • Use `cdk doctor` to detect dependency issues early
9. When should you use L1 constructs instead of L2/L3?

Expected answer points:

  • When higher-level constructs do not expose required CloudFormation properties
  • When you need precise control over every resource attribute
  • For new or niche AWS services not yet covered by L2/L3
  • Reserve for the specific property—don't use L1 exclusively
10. What is the bootstrap process in CDK and why is it needed?

Expected answer points:

  • Bootstrap creates S3 bucket and IAM roles needed for CDK toolkit
  • Required before `cdk deploy` can upload CloudFormation templates
  • Runs once per account/region: `cdk bootstrap aws://123456789012/us-east-1`
  • Without bootstrap, stack creation fails mid-deploy with permissions error
11. How do you handle secrets and sensitive data in CDK applications?

Expected answer points:

  • Never hardcode secrets in CDK code — use environment variables or AWS Secrets Manager
  • `cdk.SecretValue.secretsManager()` retrieves secrets at deployment time
  • For CI/CD, use `cdk.SecretValue.unsafePlainText()` only in non-production with caution
  • CloudFormation does not expose secret values in console after creation
  • Use AWS Systems Manager Parameter Store with encryption for configuration secrets
  • Enable CloudTrail to audit secret access patterns
12. What is the difference between CDK v1 and CDK v2? Why should you migrate?

Expected answer points:

  • CDK v2 uses stable module structure (`aws-cdk-lib` instead of `@aws-cdk/*`)
  • v2 removes experimental modules from stable API surface — fewer breaking changes
  • Better stability guarantees: experimental constructs now clearly marked
  • Simplified versioning: all `aws-cdk-lib` modules at same version
  • v1 no longer receives bug fixes or new features — security patches only
  • Migration: update imports and refactor any removed experimental APIs
13. How do you manage multi-environment deployments (dev, staging, prod) with CDK?

Expected answer points:

  • Use context values for environment-specific configuration: `app.node.tryGetContext('env')`
  • Define separate stack instances per environment: `new MyStack(app, 'MyStack-Dev')`
  • Pass environment-specific props to control replicas, instance sizes, feature flags
  • Use CDK Pipelines with separate stages for each environment
  • Cross-environment references require VPC peering or shared services in separate stack
  • Use environment variables or JSON config files to parameterize at synthesis time
14. What are CDK Aspects and when would you use them?

Expected answer points:

  • Aspects visit all constructs in a tree and apply modifications or validations
  • Use cases: add tags to all resources, check compliance (all S3 buckets encrypted), add IAM policies
  • Built-in aspects: `TagManager` for AWS tags, `AwsSolutionsCheck` for security compliance
  • Custom aspect: implement `IAspect` interface with `visit()` method
  • Aspects run after synthesis but before deployment — good for enforcement
  • Example: ensure every resource has environment tag before deployment
15. How do you prevent CDK apps from becoming slow to synthesize?

Expected answer points:

  • Deep inheritance chains slow synthesis — use composition over inheritance
  • Avoid circular construct dependencies — audit with `cdk doctor`
  • Use L3 patterns instead of composing dozens of L1 constructs manually
  • Split large apps into multiple CDK apps with separate deployment pipelines
  • Cache context values (AZs, AMI IDs) — avoid `cdk context --clear` in CI
  • For very large infrastructure (thousands of resources), consider Terraform instead
16. What is the CDK escape hatch and when should you use it?

Expected answer points:

  • CDK abstracts CloudFormation, but not all CloudFormation features are exposed
  • Escape hatch: access underlying `CfnResource` via `.node.defaultChild` to add/modify properties
  • Example: `const cfn = myBucket.node.defaultChild as s3.CfnBucket`
  • Use sparingly — escape hatch bypasses CDK validation and may break on future updates
  • Better: file GitHub issue requesting the missing feature, use escape hatch as workaround
  • Test thoroughly when using escape hatch — CloudFormation synthesis may behave unexpectedly
17. How does CDK handle dependency management between stacks?

Expected answer points:

  • Cross-stack references: `stack2.VpcId.fromStackName(stack1, 'SharedVpc')`
  • CDK automatically creates CloudFormation cross-stack references (outputs + imports)
  • Avoid circular dependencies between stacks — one stack must be deployed first
  • For complex dependencies: use AWS SSM Parameter Store or S3 as intermediate store
  • Stack dependencies affect deployment order — CDK synthesizes correct order
  • Separate stacks for separately deployed units; avoid monolith stack for large apps
18. What are the security considerations when using CDK to deploy infrastructure?

Expected answer points:

  • CDK synthesized templates are visible in CloudFormation console — no secrets in template
  • Deployment principal (who runs `cdk deploy`) needs IAM permissions for all resources created
  • Use `cdk bootstrap` with least-privilege bootstrap roles, not default admin
  • Enable `cdk diff` in CI to review changes before deployment
  • Use stack termination protection for production stacks to prevent accidental deletion
  • Pin ConstructLib versions to prevent unexpected resource changes on upgrade
19. How do you write effective unit tests for CDK constructs?

Expected answer points:

  • Use `Template.fromStack()` assertion API for property checks
  • Test resource count: `template.resourceCountByType('AWS::EC2::VPC')`
  • Test specific properties: `template.hasResourceProperties('AWS::S3::Bucket', {VersioningEnabled: true})`
  • Snapshot tests capture full synthesized template — catch unexpected changes
  • Test edge cases: empty props, maximum values, invalid combinations
  • Integration tests deploy to isolated environment for full stack validation
20. What is the CDK Construct Hub and how do you use community constructs?

Expected answer points:

  • Construct Hub (constructs.dev) aggregates community-published CDK constructs
  • Use when AWS does not have native L2/L3 construct for your use case
  • Evaluate community constructs: check maintenance status, GitHub stars, AWS validation
  • Add community construct: `npm install @author/construct-name`
  • Community constructs may have different stability guarantees than official AWS ones
  • For production: fork and maintain community constructs to prevent supply chain issues

Further Reading

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