Skip to content
Infrastructure as Code iac tools 4 min read

AWS CDK & CDKTF

The AWS Cloud Development Kit (CDK) lets you define infrastructure using a real programming language — TypeScript, Python, Java, Go, or C# — and then synthesizes a declarative template that a provisioning engine actually applies. The original CDK targets AWS CloudFormation; CDK for Terraform (CDKTF) reuses the same construct programming model but emits Terraform JSON, so you get loops, conditionals, and abstraction from code while keeping Terraform’s multi-cloud provider ecosystem and state model underneath. This “imperative generates declarative” approach is powerful when your infrastructure has a lot of repetition or logic that HCL expresses awkwardly.

How synthesis works

CDK programs do not provision anything directly. You write code that builds an in-memory tree of constructs, run a synth step, and the toolkit serializes that tree into a deployable artifact. The provisioning engine then performs the create/update/delete diffing.

ToolkitLanguage inputSynthesizes toProvisioned by
AWS CDKTS/Python/Java/Go/C#CloudFormation templatesCloudFormation
CDKTFTS/Python/Java/Go/C#Terraform JSON (cdk.tf.json)Terraform / OpenTofu

The key insight is that the loops and if statements run at synth time, on your machine. By the time the engine sees the output, it is plain declarative configuration. This keeps the apply step predictable while letting you write expressive generators.

Terraform JSON is a first-class, fully supported variant of HCL. Anything CDKTF emits can be inspected, committed, and applied with the same terraform (or tofu) binary you already use.

Constructs: the core abstraction

A construct is a reusable component that represents one or more resources. CDK defines three levels:

  • L1 (Cfn / provider) constructs map one-to-one to a raw CloudFormation resource or Terraform resource. They are auto-generated and expose every property.
  • L2 constructs are hand-written wrappers with sane defaults, helper methods, and type safety (AWS CDK only).
  • L3 (patterns) bundle multiple resources into an opinionated solution, like a load-balanced Fargate service.

In CDKTF you typically work with L1 provider constructs plus your own custom constructs for reuse.

A CDKTF example

Here is a small CDKTF stack in TypeScript that provisions an S3 bucket per environment using a normal array — the kind of repetition that is tedious in raw HCL.

import { App, TerraformStack, TerraformOutput } from "cdktf";
import { Construct } from "constructs";
import { AwsProvider } from "@cdktf/provider-aws/lib/provider";
import { S3Bucket } from "@cdktf/provider-aws/lib/s3-bucket";

class StorageStack extends TerraformStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    new AwsProvider(this, "aws", { region: "us-east-1" });

    const environments = ["dev", "staging", "prod"];
    for (const env of environments) {
      const bucket = new S3Bucket(this, `data-${env}`, {
        bucket: `devcraftly-data-${env}`,
        tags: { Environment: env, ManagedBy: "cdktf" },
      });

      new TerraformOutput(this, `bucket-arn-${env}`, {
        value: bucket.arn,
      });
    }
  }
}

const app = new App();
new StorageStack(app, "storage");
app.synth();

Running synth produces standard Terraform JSON, then you deploy with the CDKTF CLI (which shells out to Terraform under the hood):

cdktf synth
cdktf deploy storage

Output:

storage  Initializing the backend...
storage  Terraform will perform the following actions:

  # aws_s3_bucket.data-dev (data-dev) will be created
  # aws_s3_bucket.data-staging (data-staging) will be created
  # aws_s3_bucket.data-prod (data-prod) will be created

Plan: 3 to add, 0 to change, 0 to destroy.

storage  aws_s3_bucket.data-prod: Creation complete after 2s
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Outputs:
  bucket-arn-dev = "arn:aws:s3:::devcraftly-data-dev"

Because the artifact is just cdk.tf.json, you can inspect it directly:

cdktf synth --json
terraform -chdir=cdktf.out/stacks/storage plan

Since the output is ordinary Terraform configuration, you can run the same plan with OpenTofu by pointing CDKTF at the tofu binary via the terraformBinaryName setting in cdktf.json, or by invoking tofu -chdir=... against the synthesized directory.

When imperative-generates-declarative helps

Reach for CDK/CDKTF when code-level logic genuinely earns its keep:

  • You generate many near-identical resources from data (accounts, regions, microservices) and for_each in HCL becomes hard to read.
  • You want compile-time type checking, IDE autocomplete, and refactoring tools across your infrastructure.
  • Your team already writes the application in TypeScript or Python and prefers one language end to end.
  • You need to package opinionated infrastructure as installable libraries (constructs) shared across teams.

Plain HCL is often the better choice for small, stable stacks where the indirection of a synth step adds cost without payoff. Reviewers see generated JSON, not your source, so diffs in pull requests can be noisy unless you commit and review the synthesized output too.

Gotcha: CDKTF state still lives in a Terraform backend, not in your code. Configure a remote backend (S3 + DynamoDB lock, or HCP Terraform) in the stack, otherwise each developer gets isolated local state.

Best practices

  • Commit cdktf.json and lock provider versions; treat the synthesized cdk.tf.json as a build artifact but review it for surprising diffs.
  • Configure a remote backend explicitly in each stack so state and locking behave like any other Terraform project.
  • Keep synth-time logic simple and deterministic — avoid network calls or random values that change the output between runs.
  • Extract repeated resource groups into custom constructs rather than copy-pasting, so abstractions are testable in isolation.
  • Pin the CDKTF CLI and @cdktf/provider-* packages together; mismatched versions cause synth errors.
  • Run cdktf diff (which wraps terraform plan) in CI on every pull request to catch drift before merge.
Last updated June 14, 2026
Was this helpful?