Skip to content
Infrastructure as code does not mean Terraform. CDK, Pulumi, and the question that actually decides it
← ← Back to Thinking Cloud

Infrastructure as code does not mean Terraform. CDK, Pulumi, and the question that actually decides it

Somewhere around 2019, "infrastructure as code" became a synonym for Terraform in most conversations, the way "search" means Google. It is a reasonable default. It is also the wrong tool for a meaningful share of teams, and the reason is not features, which all three serious options have, but who is going to write and read the code.

We run our client platforms on AWS CDK. We have shipped Terraform and Pulumi for other clients. This is the comparison we wish we had read before choosing, organised around the question that decides it, with the tradeoffs stated plainly and the cases where we would still pick Terraform.

The question

Is infrastructure owned by the application team, or by a separate platform team?

If the people writing the Next.js app are the people writing the infrastructure, they already have a language, a package manager, a test runner and an editor set up for TypeScript. Giving them a second language (HCL) with its own semantics, its own module system and its own testing story is a cost paid every day by every person. CDK or Pulumi lets them define infrastructure in the language they already think in, in the same repository, with the same review process, and share types between the app and the infra.

If infrastructure is owned by a platform team that serves many application teams in many languages, the calculus flips. That team wants one language for all infrastructure regardless of what the apps are written in, a declarative model that is easy to review across hundreds of modules, a huge provider ecosystem, and a plan output that a non-programmer can read in a change ticket. That is Terraform's home ground, and HCL's limitations are a feature there: it is hard to be too clever in it.

Everything else in the comparison follows from that answer.

who owns the infrastructure? the application teamalready writes TypeScript · same repo · same reviewshared types between app and infraCDK if all-AWS · Pulumi if nota second language is a daily cost for every person a separate platform teamserves many teams in many languageswants plans a non-programmer can reviewTerraform · OpenTofuHCL's limits are a feature here: hard to be too clever

The three, honestly

Terraform / OpenTofu AWS CDK Pulumi
Language HCL, declarative TypeScript, Python, Java, Go, C#; synthesises to CloudFormation TypeScript, Python, Go, C#, Java, YAML; its own engine
Clouds All, via providers; the largest ecosystem AWS only (CDK for Terraform exists, but that is Terraform) All, via providers, many wrapping Terraform's
State A state file you manage: S3 + locking, or Terraform Cloud CloudFormation holds it; nothing to manage, nothing to corrupt Pulumi Cloud, or self-managed S3
Plan / diff terraform plan: precise, readable, the gold standard cdk diff: good for resources, weak for IAM and some property changes pulumi preview: good, between the two
Abstractions Modules; composition is verbose, no real types Constructs with real types; L2 constructs encode best practice (a Queue gets encryption, a Bucket blocks public access) Components, real types, plus you can import CDK constructs
Testing Plan assertions, Terratest (Go) Jest against the synthesised template; fast and precise Unit tests in-language, mocked
Failure mode State drift; a lock left behind; provider upgrades CloudFormation's limits: 500 resources per stack, slow rollbacks, replacement semantics Engine bugs are yours to debug; smaller community
Reads well for Reviewers who do not program Programmers Programmers
Who is paying HashiCorp licence changes in 2023; OpenTofu is the community fork Free; AWS's problem Free OSS; the cloud service is paid

Three things in that table decide more choices than the rest combined.

State. Terraform's state file is the source of most Terraform incidents we have been called to: a corrupted state after an interrupted apply, a stale lock, a state that references a resource someone deleted by hand, a migration between backends gone wrong. CDK has no state file; CloudFormation is the state, and it is AWS's problem to keep consistent. That alone removes an entire category of operational work. The price is CloudFormation's own quirks, which are real, but which we have never lost a state file to.

The diff. terraform plan is genuinely the best change preview in the industry, and it is a large part of why platform teams love it. cdk diff is good enough for the daily case and bad for IAM: it shows a policy changed but does not always show how. We compensate with cdk-nag and a policy of pasting the synthesised IAM into the PR for any change that touches it. If your review process is "someone who does not write code reads the plan", Terraform wins here and it is not close.

Abstractions with types. In CDK, new sqs.Queue(this, 'Q') gives you an encrypted queue with sensible defaults, and queue.grantConsumeMessages(fn) writes the IAM policy for you, correctly, with the exact ARN. The equivalent in Terraform is a module someone wrote, or twenty lines of policy JSON you write by hand and get wrong once. That difference compounds across a platform: our CDK codebase for three accounts is about 6,000 lines; the Terraform equivalent we estimated at roughly twice that, before modules.

When we would still pick Terraform

  • Multi-cloud with a platform team. One language, one workflow, every provider. Pulumi can do this too, but the Terraform ecosystem of modules and providers is a decade deeper.
  • Anything where the reviewer is not a programmer. Compliance-driven environments where the change ticket must contain a plan that an auditor reads. HCL and terraform plan were built for that.
  • Non-AWS, single cloud, small team. GCP or Azure teams without a strong TypeScript identity are usually better off with Terraform than with Pulumi, on community size alone.
  • A team that already has it and it works. Migrating working Terraform to CDK for aesthetic reasons is a bad trade. We have declined that job twice.

When Pulumi, over CDK

When the team is TypeScript-native and the estate is not purely AWS: a Cloudflare zone, a Vercel project, a Datadog monitor set, a GCP bucket, alongside the AWS resources. CDK stops at the AWS boundary; Pulumi does not. Pulumi also has the better diff of the two and a faster engine than CloudFormation for large stacks. The cost is a smaller community and a state backend you either pay for or run.

The factors that decide it factorTerraformCDKPulumi state to manageyes · most incidents live herenone · CloudFormation is the stateservice, or self-managed change previewplan · the gold standarddiff · weak on IAMpreview · good typed abstractionsmodules, no typesconstructs + grant()components, typed cloudsall · deepest ecosystemAWS onlyall non-programmer can reviewyesnono Green is a strength, red a cost you will pay. None of the columns is all green. Pick by who writes and reads the code.

What we run, and why

CDK, TypeScript, for every AWS-only client platform. The application teams are TypeScript teams; infrastructure lives in infra/ in the same monorepo as the apps; the API's route types and the infra's environment variable names come from the same packages/types; a change to a queue and its consumer is one PR. No state file, ever. cdk diff pasted into every PR, IAM synthesised and pasted for anything that touches it, cdk-nag in CI, Jest assertions on the templates for the invariants we care about (every bucket blocks public access, every queue has a DLQ, no DESTROY removal policy outside a named teardown stack).

We pay for that with CloudFormation's limits and slowness, which we have written about at length, and with the IAM-diff gap, which the paste-into-PR rule covers. For these teams it is the right trade by a wide margin. For a platform team serving twelve languages across two clouds, we would tell them Terraform, and mean it.

The short version

Infrastructure as code is a practice, not a product. The practice is: infrastructure is in a repository, changes are reviewed, nothing is clicked. The product is chosen by who owns the code. Application team in TypeScript on AWS: CDK. Same team, more than AWS: Pulumi. Platform team, many languages, plans read by non-programmers: Terraform. Working Terraform you already have: keep it.

If you are choosing, or inherited a choice that does not fit the team, we have run all three and will tell you which one fits yours, including when the answer is "the one you have".