// Stay in touch?
Products
Encore CloudEncore Cloud
Encore.tsEncore.ts
Encore.goEncore.go
PricingPricing
Book a DemoBook a Demo
Use Cases
AI-Powered DevelopmentAI-Powered Development
Event-Driven SystemsEvent-Driven Systems
Distributed SystemsDistributed Systems
Case StudiesCase Studies
ShowcaseShowcase
Resources
DocsDocs
InstallInstall
Example AppsExample Apps
Demo videoDemo video
ArticlesArticles
GitHub ReleasesGitHub Releases
Systems Operational
Company
About UsAbout Us
Swag ShopSwag Shop
ContactContact
JobsJobs
PressPress
TermsTerms
Privacy PolicyPrivacy Policy
Data Processing AgreementData Processing Agreement
Enterprise SLAEnterprise SLA
Encore
© 2026 EncoreAll rights reserved
© 2026 Encore All Rights Reserved
GitHubDiscordYouTube

AWS CDK vs Terraform: A Head-to-Head Comparison

Real languages synthesizing CloudFormation versus HCL and a multi-cloud provider ecosystem

07/08/26
11 Min Read
Ivan Cernja
07/08/26

AWS CDK vs Terraform: A Head-to-Head Comparison

Real languages synthesizing CloudFormation versus HCL and a multi-cloud provider ecosystem

Ivan Cernja
11 Min Read

Both AWS CDK and Terraform exist to solve the same problem: describing cloud infrastructure in a repeatable, version-controlled form instead of clicking through a console. They diverge on almost everything else. CDK lets you write infrastructure in TypeScript, Python, Java, Go, or C#, then synthesizes CloudFormation and deploys it to AWS. Terraform gives you a declarative language (HCL) and a provider ecosystem that reaches AWS, GCP, Azure, and hundreds of other services from one workflow.

The choice usually comes down to two questions. Are you all-in on AWS, or do you need more than one cloud? And do you want to write infrastructure in a general-purpose language, or in a purpose-built configuration language with mature tooling around it? This guide walks through how the two compare on language, cloud coverage, state, drift, and day-to-day workflow, with real examples for both.

Architecture: CloudFormation vs a Standalone Engine

CDK is a code layer on top of CloudFormation. You write constructs in a real language, run cdk synth, and CDK emits a CloudFormation template. cdk deploy hands that template to CloudFormation, which does the actual provisioning and tracks the resources. Everything CDK deploys is a CloudFormation stack underneath, which means CDK is AWS-only and inherits CloudFormation's behavior for rollbacks, state, and deploy speed.

Terraform is a standalone provisioning engine. You write HCL, Terraform builds a dependency graph, compares it against a state file, and calls provider APIs directly to create, update, or destroy resources. It never touches CloudFormation. Because the engine talks to providers through a plugin protocol, the same tool provisions an RDS database, a Cloudflare DNS record, a Datadog monitor, and a GitHub repository in one plan.

AWS CDKTerraform
LanguageTypeScript, Python, Java, Go, C#HCL (declarative DSL)
Cloud coverageAWS onlyAWS, GCP, Azure, plus 4,000+ providers
Backend engineCloudFormationTerraform's own graph engine
StateManaged by CloudFormation, no separate fileExplicit state file (local or remote backend)
Drift detectionOn-demand via CloudFormationOn every plan via refresh
Preview before applycdk diff (CloudFormation change set)terraform plan
Abstraction / reuseConstructs, full language featuresModules, limited language features
LicenseApache 2.0BSL 1.1 (OpenTofu is the MPL fork)
Deploy speedBound by CloudFormationGenerally faster, direct API calls

Code Example: The Same Stack in CDK and Terraform

Here is a small but realistic setup: an S3 bucket, a DynamoDB table, and a Lambda function that reads from both.

AWS CDK (TypeScript)

import * as cdk from "aws-cdk-lib"; import { Construct } from "constructs"; import * as s3 from "aws-cdk-lib/aws-s3"; import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; import * as lambda from "aws-cdk-lib/aws-lambda"; export class AppStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const bucket = new s3.Bucket(this, "UploadsBucket", { versioned: true, }); const table = new dynamodb.Table(this, "OrdersTable", { partitionKey: { name: "orderId", type: dynamodb.AttributeType.STRING }, billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, }); const fn = new lambda.Function(this, "Handler", { runtime: lambda.Runtime.NODEJS_20_X, handler: "index.handler", code: lambda.Code.fromAsset("lambda"), environment: { BUCKET_NAME: bucket.bucketName, TABLE_NAME: table.tableName, }, }); // One line wires up the IAM policy for both resources. bucket.grantRead(fn); table.grantReadData(fn); } }

The two grant calls are the CDK selling point. Instead of hand-writing an IAM policy document, you express intent ("this function can read this bucket") and CDK generates the least-privilege policy, the correct ARNs, and the environment wiring. That intent-level API is what AWS-native teams tend to fall in love with.

Terraform (HCL)

resource "aws_s3_bucket" "uploads" { bucket = "my-app-uploads" } resource "aws_s3_bucket_versioning" "uploads" { bucket = aws_s3_bucket.uploads.id versioning_configuration { status = "Enabled" } } resource "aws_dynamodb_table" "orders" { name = "orders" billing_mode = "PAY_PER_REQUEST" hash_key = "orderId" attribute { name = "orderId" type = "S" } } resource "aws_iam_role" "handler" { name = "handler-role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "lambda.amazonaws.com" } }] }) } resource "aws_iam_role_policy" "handler" { role = aws_iam_role.handler.id policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = ["s3:GetObject"] Resource = "${aws_s3_bucket.uploads.arn}/*" }, { Effect = "Allow" Action = ["dynamodb:GetItem", "dynamodb:Query"] Resource = aws_dynamodb_table.orders.arn }, ] }) } resource "aws_lambda_function" "handler" { function_name = "handler" role = aws_iam_role.handler.arn runtime = "nodejs20.x" handler = "index.handler" filename = "lambda.zip" environment { variables = { BUCKET_NAME = aws_s3_bucket.uploads.id TABLE_NAME = aws_dynamodb_table.orders.name } } }

The Terraform version is longer and more explicit. You write the IAM policy by hand, spell out the assume-role trust relationship, and split bucket versioning into its own resource. The payoff for that verbosity is that every AWS API concept maps to a resource block you can read top to bottom, with no synthesis step between what you wrote and what gets created. Terraform authors who have hit surprises from generated abstractions often prefer seeing exactly what will hit the API.

Language: Real Code vs Declarative Config

CDK's headline feature is that you write infrastructure in a language you already use. You get loops, conditionals, functions, classes, your editor's autocomplete, and your language's package manager. Building ten similarly configured queues is a for loop. Sharing a hardened bucket configuration across teams is publishing an npm package. For teams that think in TypeScript or Python, this removes a context switch.

That power has a cost. CDK code can hide a lot of behavior. A single high-level construct (an ApplicationLoadBalancedFargateService, say) can synthesize into dozens of CloudFormation resources, and understanding what you deployed sometimes means reading the generated template. Imperative code can also produce non-deterministic infrastructure if you are not careful, because logical IDs shift when you refactor the code that generates them.

Terraform's HCL is deliberately limited. It has variables, for_each, conditional expressions, and modules, but it is not a general-purpose language and does not pretend to be. The upside is predictability: HCL is declarative, so the configuration reads as a description of the desired end state rather than a program that computes it. The downside is that complex logic gets awkward, and DRY abstraction leans on modules that are clumsier than functions in a real language.

State and Drift

This is where the CloudFormation foundation shapes CDK's behavior the most.

CDK has no state file. CloudFormation tracks every resource in a stack inside your AWS account, so there is nothing to store, lock, or secure yourself. That removes an entire category of operational work: no S3 backend to set up, no state locking table, no risk of a corrupted or leaked state file. For teams that have been bitten by Terraform state, this is a genuine relief.

Terraform keeps an explicit state file mapping your configuration to real resource IDs. You choose where it lives (local, or a remote backend like S3 with DynamoDB locking, or a managed backend), and you are responsible for securing it because it can contain secrets. In exchange you get flexibility CloudFormation does not offer: terraform import to bring existing resources under management, terraform state mv to refactor without recreating, and state rm to drop a resource from management without destroying it.

Drift handling differs too. Terraform refreshes real resource state on every plan and shows you a diff, so drift surfaces as part of your normal workflow. CloudFormation drift detection is a separate operation you invoke on demand, and it has historically covered fewer resource properties than Terraform's refresh, so some out-of-band changes go unnoticed until a deploy fails. If catching manual console changes is important to you, Terraform's model is stronger. See Terraform drift for how teams handle this in practice.

Preview and Deploy

Both tools let you preview changes before applying them. terraform plan prints an exact resource-by-resource diff of what will be created, changed, or destroyed. cdk diff produces a CloudFormation change set and shows the same idea, though the output is in CloudFormation terms rather than the CDK constructs you wrote, which can take a moment to map back.

Deploy speed usually favors Terraform. It calls provider APIs directly and parallelizes across the dependency graph. CDK deploys go through CloudFormation, which is reliable and gives you automatic rollback on failure, but is often slower, especially for stacks with many resources or where CloudFormation serializes changes it could theoretically parallelize. The rollback safety is a real benefit; the wait is a real cost.

When to Choose AWS CDK

CDK is the stronger choice when:

  • You are committed to AWS and have no near-term plan to run on another cloud. CDK's AWS coverage is deep, though it tracks CloudFormation, which can lag new AWS service releases, and CDK's high-level constructs sometimes lag CloudFormation itself.
  • Your team wants to write infrastructure in a real language and values loops, functions, and package-based reuse over a declarative DSL.
  • You want CloudFormation's guarantees of automatic rollback and account-managed state without running your own state backend.
  • The intent-level constructs save you real work, like the grant methods that generate least-privilege IAM policies you would otherwise write by hand.

When to Choose Terraform

Terraform is the stronger choice when:

  • You run on more than one cloud, or you manage AWS alongside Cloudflare, Datadog, GitHub, Auth0, or any of the thousands of providers CloudFormation cannot touch.
  • You want an explicit, readable diff and predictable declarative config where what you write is close to what hits the API.
  • State operations matter to you: importing existing resources, moving them between modules, or refactoring without recreation.
  • Drift detection on every plan is part of how your team keeps infrastructure honest.
  • You care about tooling maturity and portability, including the OpenTofu fork if the BSL license is a concern for your organization.

There is also a middle path worth naming: CDK for Terraform (CDKTF) lets you write CDK-style code that generates Terraform configuration, giving you real languages plus Terraform's multi-cloud engine. It trades some maturity for the combination, and it is worth a look if both halves of this comparison appeal to you. For the CloudFormation angle in isolation, see Terraform vs CloudFormation.

How Infrastructure From Code Compares

If CDK appeals to you, it is usually because you would rather express infrastructure as real code than as a separate config language. That instinct points somewhere further than CDK goes.

CDK still keeps infrastructure in a separate CDK app, deployed by a separate command, described in constructs your application code knows nothing about. The Lambda function's code and the CloudFormation stack that provisions it live in different places, and neither is aware of the other. You still manage the boundary between "the app" and "the infra that runs the app" by hand.

Infrastructure from code collapses that boundary. You declare what your application needs (a database, a queue, a cron job, an object storage bucket) directly in the application's source, and the tooling provisions the matching cloud services from those declarations.

Encore is the open-source infrastructure-from-code 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("orders", { migrations: "./migrations" }); // Provisions object storage (S3 on AWS, GCS on GCP, local disk in dev). const uploads = new Bucket("uploads", { versioned: true }); export const create = api( { method: "POST", path: "/orders", expose: true }, async (req: { customerID: string }) => { const row = await db.queryRow` INSERT INTO orders (customer_id) VALUES (${req.customerID}) RETURNING id `; return row; }, );

There is no separate stack, no CloudFormation template, and no HCL. Encore parses these declarations at build time and Encore Cloud provisions the underlying AWS or GCP services in your own account. The same code runs locally with encore run, in a preview environment per pull request, and in production, and there are no runtime dependencies on Encore in production. The framework is open source, and encore build docker produces a standalone image for teams that want to leave.

Because the infrastructure lives in the application code, AI coding agents can read it the same way they read the rest of the codebase. Encore also ships an MCP server and editor rules (encore llm-rules init) that give agents schema, trace, and architecture context, so agent output follows the same conventions as hand-written code. The quick start gets a service running in a few minutes.

This does not replace CDK or Terraform for every job. Teams configuring VPCs, IAM at scale, or regulated infrastructure that does not fit the application-layer model will keep using IaC. What infrastructure from code removes is the boilerplate that grows around a typical backend, and it is especially compelling on new projects where you are not tied to an existing IaC investment. If you want the broader picture, see Encore vs Terraform, CDK, Pulumi, and SST.

Deploy with Encore

Skip both the CloudFormation and HCL layers. Deploy a TypeScript backend with Postgres and object storage to your own AWS or GCP account in minutes.

Deploy

Conclusion

For AWS-native teams who want to write infrastructure in a real language and lean on CloudFormation's managed state and rollback, CDK is a strong, well-supported choice, and its intent-level constructs remove real work. For teams that span clouds, want explicit declarative config with drift detection on every plan, or need mature state operations, Terraform earns its reputation. The decision rarely hinges on which tool is "better" in the abstract; it hinges on whether you are AWS-only, and how much you value a general-purpose language over a predictable DSL.

And if the appeal of CDK is "I want my infrastructure to be code," it is worth asking whether that code belongs in a separate stack at all, or in the application that depends on it.

Related Resources

  • AWS CDK alternatives
  • Terraform vs CloudFormation
  • CDK for Terraform guide
  • Encore vs Terraform, CDK, Pulumi, and SST

Frequently asked questions

Is AWS CDK better than Terraform?

Neither is strictly better. CDK gives AWS-native teams a real programming language (TypeScript, Python, Java, Go, C#) and deep AWS service coverage, but it only targets AWS and inherits CloudFormation's slower deploys. Terraform works across every major cloud and SaaS provider with mature state and drift tooling, at the cost of writing HCL.

Does AWS CDK use Terraform under the hood?

No. The standard AWS CDK synthesizes CloudFormation templates and deploys through CloudFormation. There is a separate project, CDK for Terraform (CDKTF), that lets you use CDK-style code to generate Terraform configuration instead, but the two are different toolchains.

Can AWS CDK manage non-AWS resources?

The standard AWS CDK is AWS-only because it compiles to CloudFormation. If you need to manage Cloudflare, Datadog, GitHub, or other non-AWS providers alongside your infrastructure, Terraform or CDKTF is the better fit.

Is Terraform harder to learn than CDK?

It depends on your background. Terraform requires learning HCL, a purpose-built configuration language. CDK lets you stay in a language you already know but requires understanding both the CDK construct model and how it maps onto CloudFormation, which adds its own learning curve.

How do CDK and Terraform handle state?

CDK delegates state to CloudFormation, which tracks resources per stack in the AWS account with no separate state file to manage. Terraform keeps its own state file (local or in a remote backend like S3), which you version, lock, and secure yourself.

Which handles drift better, CDK or Terraform?

Terraform detects drift on every plan by refreshing real resource state and showing a diff. CloudFormation (and therefore CDK) has explicit drift detection you run on demand, and it historically covers fewer properties than Terraform's refresh.

Contents
Architecture: CloudFormation vs a Standalone Engine
Code Example: The Same Stack in CDK and Terraform
AWS CDK (TypeScript)
Terraform (HCL)
Language: Real Code vs Declarative Config
State and Drift
Preview and Deploy
When to Choose AWS CDK
When to Choose Terraform
How Infrastructure From Code Compares
Conclusion
Related Resources
Encore

The backend platform for humans and agents

Build backends in Go or TypeScript and run them on your own AWS or GCP, with the guardrails your team and AI agents need to ship safely.

Ready to build your next backend?

Encore is the Open Source framework for building robust type-safe distributed systems with declarative infrastructure.