AWS CDK vs. Terraform: The trade-offs that show up in production
What you'll learn. Leave with a working mental model of AWS CDK, Terraform, and AWS CloudFormation, plus the operational differences that show up later when you are debugging drift, reviewing changes in source control, and trying to keep an AWS account predictable across teams.
Teams usually frame AWS CDK vs. Terraform as a syntax decision, and sure, one side lets you write Terraform configurations in HashiCorp Configuration Language (HCL), the other side lets you ship an AWS CDK project in TypeScript or Python with real package managers and test frameworks, but the day-to-day friction rarely comes from the characters on the screen.
It comes from what each tool treats as the "truth" about your cloud infrastructure, how it models infrastructure resources, what it considers a plan, how it stores infrastructure state, and how much abstraction you can tolerate before your cloud resources stop feeling like something you control and start feeling like something you are negotiating with.
If you're trying to define cloud infrastructure that stays understandable two years later, this is the comparison that matters.
AWS CDK vs. Terraform in one minute
AWS CDK is an AWS Cloud development kit that lets you define infrastructure using familiar programming languages, then compiles that code into AWS CloudFormation templates, and finally asks AWS CloudFormation to provision resources and manage updates on your behalf.
Terraform is an infrastructure as code (IaC) tool that lets you declare desired state in a domain-specific language, HCL, then uses a Terraform provider to talk to cloud providers directly and converge the real world to match your Terraform configurations, tracking everything it manages in Terraform state and state files that you have to treat like production data.
If you live entirely inside the AWS ecosystem and you want application code and infrastructure code to share the same software development ergonomics, AWS CDK tends to feel natural, because you can build abstractions with general-purpose programming languages and ship reusable constructs alongside the rest of your codebase.
If you care about multi-cloud, or you want one operational model for AWS, Google Cloud, and Microsoft Azure, Terraform tends to win mindshare, because the plan and apply workflow, remote backends, and provider model let you manage infrastructure across cloud services without committing your whole platform story to any single cloud native deployment engine.
AWS CDK is CloudFormation with a compiler
AWS CDK is often described as "infrastructure in code," which is accurate in the same way that "a compiler is a text editor" is accurate, because yes, you are writing code, but the thing that ultimately provisions your AWS resources is still AWS CloudFormation, in the form of generated AWS CloudFormation templates that end up in JSON format or YAML once they are synthesized.
A CDK application is a CDK app that contains one or more stacks, and each stack is just a unit of deployment that becomes a CloudFormation stack after CDK synth runs, which means CDK's execution model is really a two-step process where you run code locally to produce templates, then you hand those templates to CloudFormation to manage the lifecycle of your cloud infrastructure.
That mental model is worth holding onto because it explains a lot of the sharp edges people hit when they first try to provision infrastructure with AWS CDK code and are surprised by behavior that is actually CloudFormation behavior – like replacement semantics, rollback behavior, and limits that only show up after the template exists.
CDK shines when your team thinks in code
CDK's biggest advantage is that it lets you define infrastructure with multiple programming languages that your team already uses, so your "new project" does not begin with teaching everyone a proprietary language; it begins with choosing one of the supported programming languages and leaning on the same toolchain you already trust for application development.
That matters because the moment your infrastructure grows beyond a toy VPC, you are dealing with repeated patterns, subtle default values, and boring but essential wiring between AWS services, and code is very good at repetition, composition, and testing – especially when the abstractions are first-class and you can publish them as a library rather than copy-paste them into yet another repo.
This cloud development kit approach feels like it was designed by someone who has actually tried to build internal platforms, because a construct can hide a lot of incidental complexity – not the scary kind of hiding where the underlying resources become mysterious, but the pragmatic kind where you no longer need to remember the exact set of tags, policies, log groups, alarms, and IAM permissions that you want attached to every application load balancer.
CDK tradeoffs show up in the generated template
The tradeoff is that you are always one step removed from the thing that deploys, and the further you move away from a direct mapping between the code you wrote and the infrastructure resources CloudFormation will actually touch, the more you rely on reviewers to trust the abstraction, to read the synthesized output, or to run CDK diff and interpret what CloudFormation thinks will happen.
CDK does have a real Terraform plan equivalent in the form of CDK diff, and it can even generate an AWS CloudFormation change set to show exact changes, which is helpful when you want to know whether a security group update is an in-place change or a replacement that will drop traffic.
Still, debugging generated templates is its own skill, and you end up with a slightly odd workflow where your IaC authoring tool is one system, your provisioning engine is another, and your failure modes are often CloudFormation failure modes – this eventuality is fine if you already understand AWS CloudFormation, and frustrating if you expected CDK to insulate you from it.
Terraform is declarative IaC with providers and state
Terraform's pitch is simpler on paper, because you write Terraform code in HCL, you point it at a provider, and it provisions cloud resources by calling APIs until the world matches your desired state –the only "compiled artifact" that matters is the plan output you review before apply.
The shape of the tool encourages you to define infrastructure explicitly, which is why it's so common to see teams use Terraform for foundational AWS infrastructure such as VPCs, internet gateway attachments, NAT gateways, private subnets, IAM baselines, and shared security group patterns. They often then reuse those outputs as inputs for application stacks, because the code reads like a direct inventory of infrastructure resources.
A Terraform provider is the boundary between Terraform and the world, so your AWS provider knows how to create and reconcile AWS resources, your Azure infrastructure comes from the AzureRM provider, your Google Cloud resources come from the GCP provider, and because those providers share the same workflow, you can manage infrastructure across cloud providers without changing the operational playbook.
Terraform's operational model is state-first
Terraform's power is also its most annoying truth, because Terraform is not just files in Git (although applying GitFlow Workflow with Terraform is a good idea), it's files plus Terraform state.
Once you accept that, you stop treating state management as an implementation detail and start treating it as a core part of running IaC tools in production.
Terraform uses state to know what it already manages, to compute drift detection, and to produce a Terraform plan that reconciles desired state with real-world state, which is why remote state backends, locking, and controlled applies are not optional; they are the difference between reliable infrastructure as code and a script that sometimes works.
Terraform Cloud, now branded as HCP Terraform in the official docs, leans into that model by giving you remote runs, workspace locking, and a strict separation of plan and apply so that users can review the plan output and decide whether it gets to touch production.
Terraform’s complexity can be very frustrating, but much of it exists because cloud infrastructure itself is complex. AWS, Azure, Google Cloud: all expose thousands of stateful APIs with non-obvious dependencies, replacement semantics, and edge cases that only surface under real workloads.
Lately, there have been many attempts to "start over" when it comes to infrastructure tooling. Replacing the language or the execution engine does not remove the need to model state, detect drift, orchestrate safe updates, and reconcile out-of-band changes. Those problems do not disappear when you rewrite the core. They simply reappear somewhere else.
A full AWS CDK vs. Terraform comparison
You can compare CDK vs. Terraform across a hundred different factors, but the dimensions that change your outcomes are the ones that shape reviews, refactors, and incident response.
| Dimension | AWS CDK | Terraform |
|---|---|---|
| Authoring model | Imperative-ish code that synthesizes templates | Declarative HCL that describes the desired state |
| Provisioning engine | AWS CloudFormation | Provider APIs via Terraform |
| Scope | AWS-first | Multi-cloud by design |
| Abstraction style | Constructs with rich defaults | Modules with explicit inputs/outputs |
| Preview | CDK diff and CloudFormation change sets |
terraform plan |
| State | CloudFormation stack state | Terraform state and backends |
The imperative vs. declarative split is about reviewability
In CDK, you can loop, branch, and generate cloud infrastructure based on runtime logic, which is wonderful when you are building reusable constructs and you want to express "create N similar resources" without copy-paste. However, it also makes it easier to create infrastructure code that is hard to review, because the diff is a code diff, not an infrastructure diff, unless you insist on always reviewing the synthesized output.
In Terraform, the declarative model tends to produce stable, readable diffs for many changes because you are literally defining that the resource exists with the specific arguments, and the DSL is constrained enough that reviewers can learn its patterns.
However, that constraint is also why people reach for modules, locals, and meta-arguments to simulate programming constructs, which is how you end up with Terraform configurations that are technically declarative but feel like a programming language you did not mean to learn.
Language choice is not just developer ergonomics
AWS CDK supports TypeScript, JavaScript, Python, Java, C#, and Go, which means the supported languages are mainstream and the tooling is mature, and it is easy to integrate an AWS CDK project into existing software development workflows, whether you live in GitHub Actions, Azure DevOps, or whatever pipeline layer your org standardized on.
Terraform's language is HCL, which is a domain-specific language designed for configuration, and it is pleasant once you internalize its mental model. It is still a separate language with separate conventions, however, and the moment you ask application engineers to write Terraform configurations, you are implicitly asking them to learn a new language plus a new operational model that includes backends, locking, and import semantics.
The practical question is which learning curve you want, because CDK asks you to learn CloudFormation behavior indirectly, while Terraform asks you to learn state behavior directly.
Choosing an IaC model is an adoption decision
Infrastructure as Code rarely fails on day one. It fails months later or even years later when ownership spreads beyond the original authors and the system has to survive handoffs, reviews, audits, and incidents.
The most important question when choosing an IaC approach is not how expressive the language is, but how the system will be adopted inside a real organization with constraints.
Tools that fit naturally into existing workflows tend to spread gradually and safely. Tools that require a wholesale change in how teams operate often stall after the initial excitement, because adoption becomes an organizational problem rather than a technical one.
This is why teams with similar infrastructure needs can reach very different conclusions. The deciding factor is usually not the cloud provider or the programming language, but how well the tool aligns with the way the organization already ships and operates software.
Portability is a real constraint, even for AWS-heavy teams
If you are truly AWS-only, CDK's tight fit to the AWS ecosystem is a feature, because high-level constructs can encode best practices for AWS services, and you can build a cloud native internal platform that feels like a real SDK.
If you are even slightly multi-cloud, or you expect to acquire teams that run Azure infrastructure or Google Cloud workloads, Terraform's provider ecosystem gives you a single tool to manage infrastructure across cloud providers, and that consistency is why platform teams often pick Terraform as the default IaC tool even when most workloads are on AWS.
State and drift are where the sharp edges live
Terraform drift detection is explicit because drift is computed relative to Terraform state, and if state is wrong, outdated, or corrupted, Terraform will still try to converge, sometimes correctly, sometimes incorrectly, depending on what changed and whether the provider can safely infer intent.
CloudFormation has its own model of stack state, and CDK inherits it, which means you're not managing Terraform state files directly. You're still dealing with infrastructure state as a living thing, though, as a CloudFormation stack can drift too, and the behavior of updates still depends on what CloudFormation believes exists.
The operational lesson is boring and important, and yes, it applies to both Terraform and CDK and Terraform-adjacent tools, because every IaC system eventually becomes a story about reconciling desired state with actual state, and the only stable strategy is to reduce out-of-band changes, tighten IAM permissions, and build workflows that make it easier to do the right thing than to click around in the console.
Reuse and abstraction: CDK leans on constructs; Terraform leans on modules
CDK encourages abstraction in the form of constructs, and because they are real code, you can publish them, version them, test them, lint them, and evolve them with the same patterns you use for application libraries, which is a strong fit when you want internal platform building to look like software development.
Terraform encourages reuse through modules, which are not as expressive as code, but are often easier to reason about because the interface is explicit, and because the module boundary forces you to define inputs and outputs, which is a quiet form of governance.
Both approaches can go wrong, of course, because you can build constructs that hide too much, and you can build modules that turn into an unreadable ball of locals, dynamic blocks, and implicit defaults.
What matters is that the failure modes look different, especially when you're debugging at 2 AM.
Testing and validation depend on what you consider testable
CDK fits naturally into unit testing patterns. You can snapshot the synthesized template, assert on properties of the cloud assembly, and run tests in CI alongside application code – appealing when you want infrastructure code to be treated like a first-class software artifact.
Terraform tends to rely on plan inspection, policy checks, and environment-level tests, and while there are testing frameworks for Terraform, the default workflow still revolves around reading Terraform plan output and using that as the artifact of intent – a big reason why teams invest in automated plan checks and policy-as-code gates.
Governance and compliance is a workflow problem
If you are in a regulated environment, neither CDK nor Terraform magically gives you compliance.
Compliance is about approvals, audit trails, separation of duties, and consistent change management, which means you need pull request workflows, policy enforcement, and predictable apply behavior, regardless of whether your infrastructure code is TypeScript or HCL.
The good news is that both ecosystems support that style of governance; the bad news is that most teams do not implement it until they have an incident that forces them to care.
Abstractions fail when they demand change
Most teams already have a coordination layer, and it’s called Git. Pull requests, code reviews, CI pipelines, and audit trails are already deeply ingrained in how teams work together. This is especially true in regulated or security-conscious environments.
Abstractions that force teams to abandon these known workflows are not only awkward, but they also underestimate how much institutional knowledge is embedded in existing tooling. Every developer and developer tool already understands Git. Very few understand bespoke pieces of software.
Backwards compatibility is not a lack of ambition. It’s recognition that infrastructure tooling succeeds when it reduces workflow friction. Developers do not want to learn a new way of doing things to build out and manage cloud resources.
Licensing and ecosystem considerations
Terraform's licensing change, including the move to the Business Source License for future releases, has become a real consideration for some organizations – not because it changes how you run Terraform plan, but because it changes how you evaluate vendor risk and ecosystem direction.
You don't need to have a strong opinion about licensing to make a good tool choice, but you do need to acknowledge that this is now part of the conversation for some teams, especially those who build internal platforms that embed IaC tools.
The CDK vs. Terraform developer experience
The early phase of an IaC rollout is usually pleasant, because everything is new, the number of cloud resources is small, and the people writing the code are the people who understand it, but the real test is what happens when your AWS infrastructure becomes shared, when your platform team becomes a service provider, and when your quick abstraction becomes a dependency that half the company relies on.
Reviewing changes is where trust is built
Terraform is often praised because a Terraform plan is readable, meaning it shows you what will be created, changed, or destroyed, and reviewers can develop intuition for what a risky change looks like.
CDK can be just as reviewable, but only if you insist on reviewing the effect, not just the code. You'll typically have to capture CDK diff output in CI and teach reviewers to treat it like they treat plan output – especially when the diff is backed by a CloudFormation change set rather than a template-only comparison.
If you skip that discipline, CDK reviews become cursory and may not actually review the infrastructure.
How to avoid IaC spaghetti with abstraction boundaries
In CDK, the temptation is to build a construct for everything, because you can, and because it feels good to reduce boilerplate, but the moment your constructs start accepting dozens of optional flags, your AWS CDK code stops being a reusable platform primitive and starts being a bespoke provisioning API that only one team understands.
In Terraform, the temptation is to build a module for everything, then wire modules together with remote state reads, data sources, and implicit conventions, and you end up with an environment where writing Terraform configurations really means learning the internal module registry and hoping the module interface does what you think it does.
The rule that tends to work in practice is to:
- Keep abstractions opinionated and small
- Prefer composition over mega-modules
- Accept that sometimes the best abstraction is no abstraction, just a direct mapping to the underlying resource with a small wrapper for shared defaults.
Terraform vs. CloudFormation vs. CDK
Stop treating these tools as rivals and start treating them as layers.
AWS CloudFormation is the AWS-native template engine, which can provision AWS resources directly from templates, meaning if your organization is committed to AWS, CloudFormation is always in the room, even if you never write a template by hand.
AWS CDK is a higher-level authoring tool that generates AWS CloudFormation templates. It's essentially CloudFormation with a compiler and a construct library, making it a good fit when you want to define infrastructure with multiple programming languages and keep your infrastructure and application code in similar ecosystems.
Terraform is a separate provisioning engine that manages infrastructure state explicitly and uses providers to talk to cloud services. It's often the default answer when your cloud providers are AWS, plus Azure, plus Google Cloud, plus the SaaS tools you also depend on.
When CloudFormation alone is enough
If your stacks are simple, your team is AWS-only, and your main goal is to provision resources with minimal moving parts, CloudFormation templates can be the right level of complexity, especially if you prefer the straightforwardness of a template where every resource is explicit and you can reason about exactly what is being created.
It's not fashionable, but it is a choice, and usually boring is good.
When you need Terraform
Terraform earns its place when you need one workflow across multiple accounts, multiple environments, and sometimes multiple clouds, and you want a consistent model for review, apply, and state, especially when you are centralizing foundational infrastructure like networking, IAM permissions, and shared platform services.
That is why platform teams often end up with Terraform as the backbone, even if application teams later use CDK for app-level stacks, as the foundation wants the operational model Terraform is designed around.
A side-by-side example in CDK and Terraform
An application load balancer is a good example because it forces you to acknowledge the surrounding context, including subnets, a security group, and the fact that public load balancers live in public subnets that route through an internet gateway, while private subnets typically rely on NAT gateways for outbound access.
In CDK, the authoring experience leans on constructs and defaults, so you create an ALB and a security group and let the construct layer handle some of the glue, then you synthesize and deploy through CloudFormation.
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
export class AlbStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: cdk.StackProps) {
super(scope, id, props);
const vpc = ec2.Vpc.fromLookup(this, "Vpc", {
vpcId: "vpc-1234567890abcdef0",
});
const sg = new ec2.SecurityGroup(this, "AlbSg", {
vpc,
description: "ALB security group",
allowAllOutbound: true,
});
sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443), "HTTPS from the internet");
const alb = new elbv2.ApplicationLoadBalancer(this, "Alb", {
vpc,
internetFacing: true,
securityGroup: sg,
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
});
alb.addListener("HttpsListener", {
port: 443,
protocol: elbv2.ApplicationProtocol.HTTPS,
certificates: [
elbv2.ListenerCertificate.fromArn(
"arn:aws:acm:us-east-1:123456789012:certificate/00000000-0000-0000-0000-000000000000"
),
],
defaultAction: elbv2.ListenerAction.fixedResponse(200, {
contentType: "text/plain",
messageBody: "ok",
}),
});
}
}In Terraform, you write Terraform configurations that declare the security group, the load balancer, and the listener, and your terraform plan shows what will change before you apply, making the authoring experience more explicit.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
variable "aws_region" {
type = string
default = "us-east-1"
}
variable "vpc_id" {
type = string
}
variable "public_subnet_ids" {
type = list(string)
}
variable "certificate_arn" {
type = string
}
data "aws_vpc" "selected" {
id = var.vpc_id
}
resource "aws_security_group" "alb" {
name = "alb-sg"
description = "ALB security group"
vpc_id = data.aws_vpc.selected.id
ingress {
description = "HTTPS from the internet"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_lb" "app" {
name = "app-alb"
load_balancer_type = "application"
internal = false
subnets = var.public_subnet_ids
security_groups = [aws_security_group.alb.id]
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.app.arn
port = 443
protocol = "HTTPS"
certificate_arn = var.certificate_arn
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "ok"
status_code = "200"
}
}
}
The interesting difference is not that one is code and the other is configuration, it's that:
- CDK encourages you to rely on the construct layer and its defaults, so you need to understand what the construct is generating inside the CloudFormation templates
- Terraform encourages you to be explicit, so you need to understand how state and provider behavior will interpret your intent over time, especially when someone makes a manual change and you need to decide whether to import, ignore, or force-reconcile.
Choosing between AWS CDK and Terraform
Decision frameworks are usually presented as a checklist, but the most honest way to choose is to admit what kind of organization you are, because the tool choice is a reflection of your operating model.
| Scenario | Pick | Why |
|---|---|---|
| AWS-only product teams, heavy application focus | AWS CDK | A shared language ecosystem, easy reuse of app tooling, fast iteration |
| Platform teams managing many accounts and shared foundations | Terraform | A consistent plan/apply workflow, explicit state management, modules as interfacesm |
| Multi-cloud requirements across AWS, Azure, and Google Cloud | Terraform | A provider ecosystem and one operational model across cloud providers |
| Regulated environments | Either, with strong workflows | Approvals, audit trail, policy checks matter more than authoring syntax |
If you want a deeper view on where this industry is heading, read our thought pieces on the future of Terraform and why code still matters. Both pieces are fundamentally about the same truth: that the tool is less important than the discipline you wrap around it.
When to use AWS CDK and Terraform together
Hybrid models are common, and they work when ownership boundaries are strict.
Terraform can own foundations, including:
- VPCs
- Private subnets
- NAT gateways
- Internet gateway attachments
- Shared IAM permissions
- Org-level constructs
- Baseline observability
Those resources are cross-cutting and benefit from a consistent, state-driven workflow.
CDK can own application stacks, especially when teams want to ship AWS CDK code alongside services and treat infrastructure like a library, because it reduces context switching and lets application teams stay productive.
The pitfall is letting both tools manage the same AWS resources, because nothing good happens when two state systems believe they own the same thing – whether it is a security group rule or a route table association – so you want explicit contracts, exported values, SSM parameters, or remote-state reads, plus documentation that makes the boundaries obvious to anyone onboarding to the system.
Shipping IaC safely: the workflow layer
Most infrastructure outages caused by IaC are not caused by the tool; they are caused by uncontrolled applies, missing reviews, and teams treating production like a place where you can just try things. Following this unstructured workflow is how you end up with drift, broken state, and changes that were never recorded in source control.
A good workflow looks like this:
- Every change going through a pull request
- Plans or diffs being generated automatically and posted where reviewers can see them
- Policy checks running before apply, approvals being enforced for sensitive environments
- Applies being serialized so that two people cannot race each other in the same workspace or environment
You should take this into consideration whether you are running Terraform locally, through Terraform Cloud, or through your own CI in Azure DevOps.
If you care about drift detection, you also need to schedule it, because drift is not a one-time event, it is a constant pressure from humans, consoles, scripts, and emergency fixes
Treating drift as a periodic operational signal is how you keep infrastructure as code from becoming aspirational marketing.
Terrateam helps when Terraform is your standard
Terrateam is built for the part teams underestimate, the operational layer around Terraform where state management, concurrency, approvals, and auditability become the real bottlenecks.
When Terraform is your platform default, you want terraform plan output to appear automatically in pull requests, you want applies gated behind the right approvals, you want safe concurrency so two applies do not collide, and you want a workflow that does not require every team to reinvent the same CI logic – "standardization" that relies on copy-pasted YAML is not standardization, it is an outage waiting for a calendar invite.
AWS CDK vs. Terraform is a contract choice
AWS CDK is compelling when you want to define infrastructure in code, reuse familiar programming languages, and lean into the AWS-native deployment engine through AWS CloudFormation templates that CDK generates and manages for you.
Terraform is compelling when you want a provider-driven engine that can provision infrastructure across cloud providers, keep a consistent plan and apply workflow, and treat state as a first-class artifact that you protect and govern like production data.
Either way, the tool will not save you from the hard parts, because the hard parts are ownership boundaries, drift detection, controlled applies, and making sure infrastructure changes flow through source control instead of someone's memory.