← Back to Blog
28 Mar 2026 Architecture 8 min read

Why I Use CDK Over Terraform
(And When I Don't)

Pardeep By Pardeep Dhingra

I've shipped infrastructure with both AWS CDK and Terraform in production. Not demo apps, not side projects—real systems handling real traffic with real on-call rotations. And after years of living with both, I have a clear preference: CDK wins for my teams, most of the time. But "most of the time" is doing heavy lifting in that sentence, and the exceptions matter.

This isn't a "CDK good, Terraform bad" post. Both tools solve the same problem—declarative infrastructure as code—but they make fundamentally different bets about how developers should interact with cloud resources. Those bets have real consequences for velocity, debugging, and team cognitive load.

The Core Difference

Terraform uses HCL, a purpose-built configuration language. CDK uses general-purpose programming languages (TypeScript, Python, Go, etc.) that synthesize down to CloudFormation. This isn't a superficial distinction—it shapes everything from how you handle conditionals to how you compose reusable abstractions.

HCL was designed to be readable by anyone. CDK was designed to be powerful for developers. That tension is the entire debate.

Side-by-Side: The Same Infrastructure

Here's an API Gateway backed by a Lambda function with a DynamoDB table. Click the tabs above each block to see different resource definitions.

CDK · TypeScript
const fn = new NodejsFunction(this, 'Handler', { entry: 'src/handler.ts', runtime: Runtime.NODEJS_20_X, environment: { TABLE_NAME: table.tableName, }, }); const api = new RestApi(this, 'Api'); api.root .addResource('items') .addMethod('GET', new LambdaIntegration(fn)); // 12 lines. Bundling, IAM, API GW // integration — all handled.
Terraform · HCL
resource "aws_lambda_function" "handler" { filename = "handler.zip" function_name = "items-handler" role = aws_iam_role.lambda.arn handler = "index.handler" runtime = "nodejs20.x" environment { variables = { TABLE_NAME = aws_dynamodb_table.tbl.name } } } resource "aws_api_gateway_rest_api" "api" { name = "items-api" } # + resource, method, integration, # deployment, stage... ~50 more lines
CDK · TypeScript
const table = new Table(this, 'Items', { partitionKey: { name: 'pk', type: AttributeType.STRING, }, billingMode: BillingMode.PAY_PER_REQUEST, removalPolicy: RemovalPolicy.DESTROY, }); // Type-safe. Enum values. IDE autocomplete // catches typos at write-time, not deploy.
Terraform · HCL
resource "aws_dynamodb_table" "tbl" { name = "items" billing_mode = "PAY_PER_REQUEST" hash_key = "pk" attribute { name = "pk" type = "S" } } # String values. Typo in "PAY_PER_REQEUST"? # You'll find out at `terraform apply`.
CDK · TypeScript
// One line. CDK generates the IAM policy // with least-privilege scoping. table.grantReadWriteData(fn); // That's it. The L2 construct knows // exactly which actions DynamoDB needs // and scopes to the table ARN.
Terraform · HCL
resource "aws_iam_role" "lambda" { name = "lambda-role" assume_role_policy = jsonencode({...}) } resource "aws_iam_role_policy" "db" { role = aws_iam_role.lambda.id policy = jsonencode({ Statement = [{ Effect = "Allow" Action = ["dynamodb:GetItem", "dynamodb:PutItem", ...] Resource = aws_dynamodb_table.tbl.arn }] }) } # Manual policy. Easy to over-permission.

The pattern is consistent: CDK's L2 constructs collapse 40-60 lines of HCL into 8-15 lines of TypeScript. But the line count isn't the point. The point is that CDK encodes AWS best practices into the abstraction itself—least-privilege IAM, encryption defaults, proper integration wiring. With Terraform, you're responsible for every detail.

When CDK Wins

After running CDK in production across multiple teams, these are the conditions where it consistently outperforms Terraform:

Your team already writes TypeScript

This is the biggest factor. If your backend engineers write TypeScript daily, CDK isn't a new tool—it's the same language with a different import path. No HCL syntax to learn, no separate toolchain to maintain. Your team's existing knowledge of interfaces, generics, and async patterns directly transfers to infrastructure code.

The developer experience gap is real: autocomplete, go-to-definition, inline type errors, and refactoring support all work out of the box. When a junior engineer misspells billingMode, TypeScript catches it at compile time. In HCL, you find out during terraform apply—or worse, in production.

You need complex, reusable abstractions

CDK's L3 constructs let you compose infrastructure like you compose software. Need a standard "microservice" pattern that bundles a Lambda, API Gateway, DynamoDB table, CloudWatch alarms, and X-Ray tracing? Write it once as a construct, publish it to your internal registry, and every team gets a production-ready stack in five lines.

Terraform modules can do this too, but HCL's limited expressiveness makes complex conditional logic painful. Try writing a module that conditionally creates different resource configurations based on environment, feature flags, and team ownership. In CDK, it's just if statements. In HCL, it's count, for_each, and dynamic blocks fighting each other.

You're all-in on AWS

CDK is a first-party AWS tool. New services and features land in CDK constructs within days of GA. The L2 constructs encode AWS-specific patterns—VPC CIDR allocation, ECS task definition nuances, Lambda layer versioning—that Terraform providers take weeks or months to support properly.

If you're running multi-cloud (and actually using it, not just planning to), this advantage disappears. But if your infrastructure is AWS-only—and for most startups and scale-ups, it is—CDK's depth of AWS integration is a real productivity multiplier.

You want infrastructure tests that feel like unit tests

CDK lets you write assertions against synthesized CloudFormation templates using standard test frameworks. Want to verify that every Lambda function has a timeout under 30 seconds, or that no S3 bucket allows public access? Write a Jest test. It runs in milliseconds with no cloud API calls.

Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Timeout: Match.numberLessThan(30) })

Terraform's testing story has improved with terraform test, but it still requires actual infrastructure provisioning for meaningful validation. CDK's synthesis model means you can test infrastructure logic the same way you test application logic.

When Terraform Wins

Now the uncomfortable part. There are real scenarios where I reach for Terraform instead, and pretending otherwise would be dishonest.

The Honest Comparison

These bars reflect my experience across production systems. Scroll to animate.

CDK Terraform
Developer DX
92
60
Multi-cloud
15
95
State Mgmt
45
88
Ecosystem
55
90
Abstraction
90
55
Debugging
40
82
Hiring
35
85
Testing
85
50

Three areas where Terraform's lead is insurmountable:

State management. Terraform's state file is explicit, inspectable, and portable. You can terraform state list, terraform state mv, and terraform import resources with surgical precision. CDK delegates state to CloudFormation, which means you're at the mercy of CloudFormation's rollback behavior. When a CDK deploy fails mid-stack, the debugging experience is noticeably worse. You're reading CloudFormation events, not CDK code.

Multi-cloud. If you genuinely manage infrastructure across AWS, GCP, and Azure—or you manage non-cloud resources like Datadog dashboards, PagerDuty rotations, or GitHub repos—Terraform's provider ecosystem is unmatched. CDK is AWS-only. CDKTF exists but it's an adapter layer, not a first-class experience.

Hiring. Terraform has an order of magnitude more practitioners. If you're building a platform team that needs to hire DevOps engineers quickly, the Terraform talent pool is significantly larger. CDK is growing but still niche.

The Debugging Problem Nobody Talks About

This is my biggest honest critique of CDK. When things go wrong—and in infrastructure, things always go wrong—CDK adds an abstraction layer between you and the actual error.

The CDK debugging chain: Your TypeScript → CDK constructs → synthesized CloudFormation → CloudFormation execution → AWS API calls. When a deploy fails, the error message comes from CloudFormation, not CDK. You need to mentally map backwards through two abstraction layers to find the root cause. Terraform's chain is shorter: your HCL → Terraform provider → AWS API calls.

I've lost hours to CDK errors that boiled down to "CloudFormation says this resource already exists" with no indication of which CDK construct caused it. The cdk diff command helps, but it's not enough when you're debugging a failed deployment at 2am.

The Decision Framework

Click each question to see the reasoning. Follow the path that matches your situation.

Is your team primarily TypeScript/Python developers?
If your team writes application code in TypeScript or Python daily, CDK's same-language advantage is significant. They won't need to context-switch to a different DSL for infrastructure.
YES ↓NO ↓
Do you need multi-cloud?
Real multi-cloud, not "we might someday." If you're managing GCP, Azure, or third-party SaaS resources alongside AWS, Terraform's provider ecosystem is the only mature option.
Use Terraform
NO ↓YES ↓
Do you build reusable infra constructs?
If you have 3+ teams sharing infrastructure patterns (standard Lambda microservice, standard ECS service, standard data pipeline), CDK constructs are dramatically easier to compose and maintain than Terraform modules with complex variable passing.
Use Terraform
Use CDK
Either works

What I'd Choose Today

If I'm starting a new AWS-only project with a TypeScript team, I pick CDK every time. The developer experience, type safety, and construct library make our team faster. The code reads like application code because it is application code, and that consistency across the stack reduces cognitive load.

If I'm joining a team with existing Terraform infrastructure, I don't rewrite it. Terraform's ecosystem maturity, state management, and the existing team knowledge are real assets. The migration cost almost never justifies the benefit.

If I'm building a platform team that manages infrastructure for non-AWS services—monitoring, CI/CD, DNS, CDN—I use Terraform for the multi-provider support.

The uncomfortable truth: The best IaC tool is the one your team can debug at 3am without calling the person who wrote it. Type safety and abstractions matter during development. Debuggability and state transparency matter during incidents. Weigh accordingly.

The Tradeoffs I've Accepted

Every CDK project I run, I accept these tradeoffs consciously:

CloudFormation limits are CDK limits. 500 resources per stack is a hard ceiling. You work around it with nested stacks or stack splitting, but it adds complexity. Terraform has no equivalent limit.

The abstraction can leak. When an L2 construct doesn't expose a property you need, you drop down to L1 (raw CloudFormation) or use escape hatches. This is jarring and creates inconsistency in your codebase.

Deploy times are slower. CDK synthesizes to CloudFormation, then CloudFormation orchestrates the deployment. Terraform talks directly to AWS APIs. For large stacks, CDK deploys can take 2-3x longer.

The community is smaller. Stack Overflow answers, blog posts, and third-party modules are less abundant. When you hit an edge case, you're more likely to be reading CDK source code on GitHub than finding a solved answer.

I accept all of these because the development velocity and type safety outweigh them for my use cases. Your calculus might be different, and that's fine. The worst thing you can do is pick a tool based on someone else's blog post instead of your own team's constraints.

Including this one.