If you like Terraform's provider coverage but not HCL, CDK for Terraform (CDKTF) was HashiCorp's official answer. You write infrastructure in TypeScript, Python, Java, C#, or Go, and CDKTF turns that code into Terraform JSON that the normal Terraform engine plans and applies. You keep every provider in the Terraform Registry and get loops, conditionals, types, and your editor's autocomplete instead of HCL's expression language.
One fact frames everything else in 2026: HashiCorp deprecated CDKTF on December 10, 2025. The project is archived and read-only, it receives no further fixes or compatibility updates, and HashiCorp recommends migrating to standard Terraform and HCL. If you are choosing a tool today, treat CDKTF as a legacy option. This guide covers what CDKTF does, a complete TypeScript example, the tradeoffs that matter in practice, and where it fits now that it is unmaintained.
CDKTF is a code-generation and synthesis layer. It is built on the same Cloud Development Kit (CDK) construct model as AWS CDK, but instead of emitting CloudFormation, it emits Terraform JSON (cdk.tf.json). Everything below that JSON is stock Terraform: the same engine, the same providers, the same state file, the same plan and apply lifecycle.
So CDKTF is a front end for Terraform rather than a replacement for it. When you run cdktf deploy, the flow is:
cdktf.out/.This matters for expectations. Anything that is a Terraform concept (state, backends, providers, the resource graph, drift) is still your concern with CDKTF. You have put a typed language in front of Terraform, and Terraform is all still there underneath.
The three tools get conflated constantly. Here is how they differ.
| CDKTF | AWS CDK | Pulumi | |
|---|---|---|---|
| Languages | TS, Python, Java, C#, Go | TS, Python, Java, C#, Go | TS, Python, Go, C#, Java, YAML |
| Synthesizes to | Terraform JSON | CloudFormation | Nothing; direct engine |
| Engine underneath | Terraform | CloudFormation | Pulumi engine |
| Providers | Terraform Registry (3000+) | AWS only | Pulumi + Terraform bridge |
| State | Terraform state file | Managed by AWS | Pulumi state (self-host or cloud) |
| Multi-cloud | Yes | No | Yes |
| Community size | Smallest of the three | Large (AWS only) | Large |
The key line in that table is "engine underneath." AWS CDK and CDKTF are both thin construct layers that generate config for an existing engine. Pulumi ships its own engine and talks to cloud APIs directly. That is why Pulumi feels like one tool and CDKTF feels like two.
Here is a realistic CDKTF stack: an AWS VPC, a subnet, and an S3 bucket with versioning. This is the kind of thing you would write in HCL, expressed as a TypeScript class.
First, initialise a project:
npm install -g cdktf-cli
mkdir infra && cd infra
cdktf init --template=typescript --local
cdktf provider add "aws@~>5.0" --force-local
--local uses local state; drop it to use HCP Terraform. --force-local generates typed bindings for the AWS provider under .gen/; without it, provider add installs the prebuilt @cdktf/provider-aws npm package instead, which you would import from @cdktf/provider-aws/....
Now the stack itself, in main.ts:
import { Construct } from "constructs";
import { App, TerraformStack, TerraformOutput, S3Backend } from "cdktf";
import { AwsProvider } from "./.gen/providers/aws/provider";
import { Vpc } from "./.gen/providers/aws/vpc";
import { Subnet } from "./.gen/providers/aws/subnet";
import { S3Bucket } from "./.gen/providers/aws/s3-bucket";
import { S3BucketVersioningA } from "./.gen/providers/aws/s3-bucket-versioning";
class NetworkStack extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
new AwsProvider(this, "aws", { region: "us-east-1" });
// Remote state in S3 instead of a local file.
new S3Backend(this, {
bucket: "my-tf-state",
key: "network/terraform.tfstate",
region: "us-east-1",
});
const vpc = new Vpc(this, "main-vpc", {
cidrBlock: "10.0.0.0/16",
tags: { Name: "main" },
});
// Real language features: a loop over availability zones.
const azs = ["us-east-1a", "us-east-1b", "us-east-1c"];
azs.forEach((az, i) => {
new Subnet(this, `subnet-${az}`, {
vpcId: vpc.id,
cidrBlock: `10.0.${i}.0/24`,
availabilityZone: az,
});
});
const bucket = new S3Bucket(this, "assets", {
bucket: "my-app-assets-2026",
});
new S3BucketVersioningA(this, "assets-versioning", {
bucket: bucket.id,
versioningConfiguration: { status: "Enabled" },
});
new TerraformOutput(this, "bucket_name", { value: bucket.bucket });
}
}
const app = new App();
new NetworkStack(app, "network");
app.synth();
Then run it:
cdktf synth # writes Terraform JSON to cdktf.out/
cdktf deploy # runs terraform apply under the hood
The azs.forEach loop is the point. In HCL you would reach for count or for_each with the count-index gymnastics that come with them. In CDKTF it is an ordinary array iteration, and vpc.id is a typed reference the editor can autocomplete and the compiler can check.
For comparison, the same resources in HCL:
provider "aws" {
region = "us-east-1"
}
terraform {
backend "s3" {
bucket = "my-tf-state"
key = "network/terraform.tfstate"
region = "us-east-1"
}
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = { Name = "main" }
}
variable "azs" {
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
resource "aws_subnet" "this" {
for_each = { for i, az in var.azs : az => i }
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${each.value}.0/24"
availability_zone = each.key
}
resource "aws_s3_bucket" "assets" {
bucket = "my-app-assets-2026"
}
resource "aws_s3_bucket_versioning" "assets" {
bucket = aws_s3_bucket.assets.id
versioning_configuration {
status = "Enabled"
}
}
output "bucket_name" {
value = aws_s3_bucket.assets.bucket
}
The HCL is shorter for this small case. CDKTF's advantage grows with complexity: when you have real conditionals, shared helper functions, tested abstractions, and constructs you want to reuse across teams with type safety, the language buys you more than it costs. For a handful of resources, HCL wins on line count.
CDKTF's problem is that it inherits the downsides of both worlds it bridges.
You still have Terraform state. State locking, drift, terraform state rm, backend configuration, and the whole operational surface of state management come along unchanged. CDKTF does not simplify any of it. If state management is your pain with Terraform, CDKTF does not address it. See Terraform state drift for what that surface looks like.
You have added an abstraction layer. When something breaks, you now debug across two levels: the TypeScript that produced the JSON, and the Terraform plan that the JSON produced. A confusing diff might live in your loop logic or in how a provider binding maps to a resource. The synthesized cdktf.out/ JSON is verbose and not meant for humans, so reading it to understand a problem is unpleasant.
Provider docs are written for HCL. Every provider in the Terraform Registry documents its resources in HCL. CDKTF generates bindings from those provider schemas, so a resource attribute like cidr_block becomes cidrBlock, for_each behaves differently, and nested blocks map to nested objects in ways you have to reverse-engineer from the generated types. You spend real time translating HCL examples into the CDKTF shape.
The community is smaller than either parent. HCL Terraform has enormous adoption and a deep well of examples, modules, and Stack Overflow answers. Pulumi has a large, active community around its own model. CDKTF sits between them with the smallest user base of the three. When you hit an edge case, there are fewer people who have hit it before you.
It is no longer maintained. HashiCorp deprecated CDKTF on December 10, 2025 and archived the repository. It will not get bug fixes, security patches, or compatibility updates for new Terraform or provider versions. Anything you build on it is built on a frozen dependency.
The rest of this section describes the fit CDKTF had while it was maintained. If you are choosing a tool in 2026, its deprecation weighs against all of it: you would be adopting a dead project, and HashiCorp points new users at standard Terraform and HCL. If only the taste for a real programming language describes you, Pulumi is the cleaner choice. See migrating from Terraform to Pulumi if that is the direction you are leaning.
CDKTF fit teams that:
Stay on HCL Terraform if:
Look at Pulumi instead if:
For a broader field, see AWS CDK vs Terraform and Terraform vs Pulumi.
If the reason you are looking at CDKTF is "I want to define my infrastructure in TypeScript," it is worth knowing that there is a more direct version of that idea.
CDKTF puts a TypeScript layer in front of Terraform, but the infrastructure code still lives in its own project, separate from your application, with its own state file and its own apply step. Your application code and your CDKTF code do not know about each other.
Infrastructure from Code (IFC) collapses that separation. You declare the infrastructure your application needs (a Postgres database, a Pub/Sub topic, a cron job, an object storage bucket) directly in the application's source code, and the tooling provisions the matching cloud services from those declarations. There is no separate infra project and no separate state file to reconcile.
Encore is the open-source IFC framework for TypeScript and Go, with over 12,000 GitHub stars and production users including Groupon.
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { Bucket } from "encore.dev/storage/objects";
// Provisions managed Postgres (RDS on AWS, Cloud SQL on GCP, Docker locally).
const db = new SQLDatabase("assets", { migrations: "./migrations" });
// Provisions an S3 bucket on AWS (or GCS on GCP).
const files = new Bucket("uploads", { versioned: true });
export const upload = api(
{ method: "POST", path: "/upload", expose: true },
async (req: { name: string; data: string }) => {
await files.upload(req.name, Buffer.from(req.data, "base64"));
await db.exec`INSERT INTO assets (name) VALUES (${req.name})`;
return { stored: req.name };
},
);
Encore Cloud provisions the underlying AWS or GCP resources in your own cloud account. There is no separate Terraform JSON, no cdktf.out/, and no state file to manage. The open-source tooling parses the application code, so there is nothing to reconcile against a separate state model. The same code runs locally with encore run, in a preview environment per pull request, and in production, with the application as the single source of truth for both behaviour and infrastructure. See the TypeScript quick start to try it.
This is not a replacement for CDKTF in every case. If you are managing large fleets of VMs, complex networking, or IAM policies that do not map to application-level primitives, you will keep an IaC tool alongside it. What IFC replaces is the configuration project that grows alongside a typical backend, and it is especially compelling for new services where you are not already locked into a Terraform investment. For the wider comparison, see Encore vs Terraform, CDK, Pulumi, and SST.
CDKTF's friction with coding agents comes from the split the section above describes. The infrastructure lives in a separate project, and provider bindings are generated from HCL-oriented schemas, so an agent has to translate registry examples into the CDKTF shape and cannot see how a resource maps to the application that uses it.
Because Encore declares infrastructure in the application code, an agent reads the database, bucket, and API definitions in the same files as the logic that calls them. Encore also ships an MCP server and editor rules (encore llm-rules init) that give an agent the schema, request traces, and service architecture as context. The framework's conventions are consistent, so agent-generated services tend to come out in a deployable shape rather than as config an agent guessed at. See writing TypeScript backends with AI for how that works in practice.
Skip the synthesis step entirely. Deploy a TypeScript backend with Postgres and object storage to your own AWS or GCP account in minutes.
CDKTF is a tool from HashiCorp that lets you define infrastructure in TypeScript, Python, Java, C#, or Go instead of HCL. It synthesizes your code into Terraform JSON and then uses the standard Terraform engine and providers to plan and apply changes.
No. AWS CDK synthesizes to CloudFormation and only targets AWS. CDKTF reuses the same construct programming model but synthesizes to Terraform JSON, so it works with any of Terraform's 3000+ providers, not just AWS.
Yes. CDKTF is a code layer on top of Terraform. It produces Terraform JSON, and the Terraform engine still manages a state file and runs the same plan and apply lifecycle underneath.
No. HashiCorp deprecated CDK for Terraform on December 10, 2025 and no longer supports or maintains it; the repository is archived and read-only. CDKTF reached general availability in August 2022 at version 0.12 and never shipped a 1.0 release. For new work HashiCorp recommends migrating to standard Terraform and HCL.
Pulumi is a self-contained tool with its own engine and state backend. CDKTF is a wrapper that generates Terraform and reuses the Terraform engine, so you inherit Terraform's provider ecosystem and its state model. Choose CDKTF if you are committed to Terraform providers; choose Pulumi if you want a single integrated tool.
Yes. CDKTF can consume existing Terraform and HCL modules and providers directly. It generates typed bindings for them so you can call a published module from TypeScript or Go.