Back to Blog
AWS Cloud Infrastructure DevOps CI/CD

Terraform vs. AWS CDK vs. CloudFormation: How We Actually Choose

The IaC tool you pick shapes your team for years. Here's the decision framework we use across real client infra — by team, blast radius, and language fit — with honest trade-offs.

Terraform vs. AWS CDK vs. CloudFormation: How We Actually Choose

You’re standing up infrastructure for a new project and someone asks the question that quietly decides the next two years: “Are we doing this in Terraform or CDK?” Pick wrong and you’ll feel it every time you onboard an engineer, debug a failed deploy, or try to share a module across teams.

The advice you’ll find online is mostly tribal — Terraform people say Terraform, AWS people say CDK, and almost nobody admits CloudFormation still has a job. We provision real infrastructure across all three, for clients with very different teams and constraints, and the honest answer is that the tool matters less than matching it to the team and the blast radius — and that the best setups often mix more than one rather than purify around a single choice. Here’s the framework we actually use.

First, separate the three things people conflate

These tools don’t sit on the same axis, which is half of why the debate goes in circles.

  • CloudFormation is AWS’s native declarative engine. You describe resources in YAML/JSON, AWS reconciles them, and the state lives inside AWS as a stack. No external state file, no extra tooling — but AWS-only, and verbose.
  • AWS CDK is not a separate engine. It’s a set of imperative libraries (TypeScript, Python, Java, Go, C#) that synthesize down to CloudFormation. You write new s3.Bucket(this, 'Assets') and CDK emits the template CloudFormation deploys. So CDK inherits every CloudFormation strength and every CloudFormation limitation. (This is AWS CDK — not the separate CDK for Terraform, CDKTF, which synthesizes to HCL instead.)
  • Terraform (and its fork, OpenTofu) is a separate declarative engine with its own HCL language, its own state file, and a provider model that reaches far past AWS — Cloudflare, Datadog, GitHub, your DNS, your SaaS billing.

That last distinction is the one that actually drives most of our decisions. CDK and CloudFormation are AWS deployment tools. Terraform is a universal control plane. If your world is one AWS account, that difference is theoretical. If your world is three clouds plus a dozen SaaS providers, it’s everything.

The decision, in the order we work it

We don’t start from “which language is nicest.” We start from constraints that are expensive to get wrong.

1. Is your stack AWS-only, or genuinely multi-provider?

This is the first fork. If you’re managing AWS and Cloudflare DNS, and a Datadog config, and GitHub repos as code, Terraform’s provider ecosystem means one tool, one workflow, one state model for all of it. We reach for Terraform by default on infrastructure that crosses provider boundaries — which, for most clients past the early stage, it does.

If you are realistically AWS-only for the foreseeable future, that advantage evaporates and the other factors decide.

2. How strong is your team’s language fit?

CDK’s pitch is real: if your team lives in TypeScript or Python, you write infrastructure in a language you already know, with loops, conditionals, real abstractions, and your editor’s autocomplete. A team that finds HCL’s for_each and dynamic blocks awkward will often be genuinely more productive in CDK.

The trade-off is that imperative power cuts both ways. We’ve inherited CDK codebases where a clever abstraction made it impossible to tell what would actually deploy without running cdk synth and reading 4,000 lines of generated CloudFormation. HCL is more constrained, and that constraint keeps the infrastructure readable even when the code is less elegant. Declarative-but-boring is a feature when the person debugging at 2 a.m. isn’t the person who wrote it.

3. How many engineers, and how separated do they need to be?

State and blast radius scale differently across the three:

  • CloudFormation/CDK keep state inside AWS, scoped per stack. Cross-stack sharing happens through exports/imports or SSM parameters, which is safe but rigid — you can’t easily delete a stack whose outputs another stack imports. Drift detection exists but is clunky.
  • Terraform keeps state in a file you must store and lock yourself (S3 with native lockfile locking, which no longer requires a separate DynamoDB table, or HCP Terraform). That’s an operational responsibility — but it also gives you precise control over workspaces and module boundaries, which is how you keep one team’s terraform apply from being able to touch another team’s database.

For a two-person startup, the CloudFormation “state is just handled” model is one less thing to run. For a platform team carving infrastructure into independently-owned domains, Terraform’s explicit state and module system is the thing that makes that separation enforceable.

4. How much do you fear drift and lock-in?

Every IaC tool drifts when someone clicks in the console. Terraform’s plan is, in our experience, the clearest preview of what’s about to change — it’s the single feature engineers miss most when they move to CDK, where cdk diff shows a CloudFormation-level diff that’s noisier and harder to read. On the other hand, CloudFormation’s rollback is genuinely better: a failed deploy rolls the stack back to its last good state automatically. A failed terraform apply can leave you half-applied, holding the pieces.

On lock-in: CDK ties you to AWS by construction. Terraform doesn’t lock you to a cloud, but the 2023 license change pushed many teams to OpenTofu, the open-source fork — which we now treat as the default for new Terraform work, since it’s a drop-in for existing configs and removes the licensing question entirely.

How this plays out in practice

The framework collapses into a few common shapes:

  • Early-stage startup, small team, all-in on AWS, TypeScript shop. CDK. They get to use the language they know, AWS handles state and rollback, and “multi-cloud someday” isn’t worth paying for today. We’ll revisit if they outgrow it.
  • Growing company, multiple AWS accounts plus Cloudflare/DNS/SaaS as code, a real platform team. Terraform (OpenTofu). One tool spans every provider, module boundaries map to team ownership, and plan keeps a larger team honest about what each change does.
  • A single, self-contained AWS service or a tightly-scoped add-on. Often just CloudFormation. No state to manage, native rollback, and nothing to learn if the team already reads YAML. We don’t reach for a heavier tool than the job needs.

Notice none of these is “Terraform is best” or “CDK is the future.” Each is a match between a tool and a team’s actual constraints.

The trade-offs nobody puts on the comparison table

A few honest things the marketing pages skip:

  • CDK’s generated templates can hit CloudFormation’s limits. Because everything synthesizes to CloudFormation, you inherit its resource-per-stack ceilings and its slower, less granular deploys. A big CDK app can produce templates that are painful to debug precisely because you didn’t write them.
  • Terraform state is a real operational asset — protect it. A lost or corrupted state file is a bad day. Remote state with locking isn’t optional past one engineer, and it’s the step teams most often skip until it bites them.
  • Mixing tools is normal and fine. Plenty of mature setups run Terraform for the account scaffolding and networking, and CDK or CloudFormation for individual application stacks. Purity isn’t the goal; clear boundaries are.
  • Whatever you pick, it lives in CI. None of these tools delivers its value run from a laptop. The win comes when plan/diff runs on every pull request and apply runs on merge — the same discipline we apply to application deploys in Automating cloud deployments with GitHub Actions, S3, and CloudFront.

The choice is about your team, not the tool

All three of these tools work. We ship production infrastructure with each of them. The teams that regret their IaC choice almost never picked a bad tool — they picked one that didn’t fit how their team works, how many providers they touch, or how much state they’re prepared to own.

So run the questions in order: multi-provider or AWS-only, language fit, team size and separation, tolerance for drift versus operational overhead. The answer usually falls out before you’ve argued about syntax. And the same architectural thinking applies one level up — whether a workload should be serverless or always-on shapes what you’re provisioning in the first place, which we worked through in Serverless vs. Traditional Servers, and the right compute choice is where savings like our 64% AWS cost reduction on Graviton2 actually come from.

If you’re standing up new infrastructure and want a second opinion on the tool — or on the architecture underneath it — that’s the kind of decision we help teams make.