ISSUE 2026-09-27SERIES BREC 50TYPE BRIEFgarnetgrid.com
They are not three versions of the same tool
Insights
HCL versus TypeScript is the least important part of this decision. What you are really choosing is an engine, a state model and a recovery procedure, and those differ far more between the three than the syntax does. Here is what each one actually commits you to, and what tends to go wrong once it is in production.
The question is usually argued as a language preference, which is why it gets answered badly. You are not choosing between HCL, TypeScript and Python. You are choosing which engine applies your changes, where the record of what you have built is kept, who is allowed to break it, and whether the person on call at two in the morning can read the diff and tell what is about to happen. Those differ far more between Terraform, Pulumi and CDK than the syntax does.
Terraform takes declarative HCL, builds a dependency graph, and calls provider plugins that wrap real APIs. The engine runs wherever you run the binary: a laptop, a CI runner, a hosted service. Nothing outside your control decides whether an apply succeeds.
Pulumi executes your program, and every resource the code registers becomes part of the desired state, which the engine reconciles against a state backend. Many of its providers are Terraform providers wrapped through a bridge, so much of the behaviour, and many of the bugs, are inherited from the same code Terraform runs.
CDK is the different one. It is not a deployment engine. It synthesises CloudFormation templates and hands them to CloudFormation, which does the work. You write TypeScript, but the thing that decides whether a change applies, stalls or rolls back is an AWS service you do not run. CDK is an excellent template compiler with CloudFormation's operational characteristics attached. You are choosing both.
The same model has been pointed at other engines: CDK for Terraform emits HCL, cdk8s emits Kubernetes manifests. Model and engine are separable, and the engine decides how bad your worst day is.
State is the decision you are actually making
Terraform and Pulumi hand you the state and the responsibility for it. State is not metadata. For many resource types it holds generated passwords, private keys and connection strings in plaintext. The bucket holding it is a credential store whether you treat it as one or not: encrypt it, restrict who can read it, and keep an audit trail.
Locking is where the ordinary bad day happens. A CI job that dies mid-apply leaves the lock held, and the next engineer meets an error message rather than a procedure. Write the procedure down first: who may break a lock, and what they must check first.
CloudFormation inverts the trade. AWS holds the state, so you cannot corrupt it with a careless command and you cannot repair it either. When a stack reaches a rollback-failed condition your options are the ones the service offers: continue the rollback while skipping the resources that will not move, or delete the stack and rebuild. On a stack containing a production database, deleting is not an option, which is why removal policies and stack boundaries matter far more in CDK than people expect on day one.
Terraform and Pulumi give you sharp tools that can worsen an incident and can also end it in five minutes. CloudFormation gives neither.
The language argument cuts both ways
What a real language buys is real: types at the boundaries, unit tests on a construct, one abstraction reused across forty services, and a loop you can read instead of a for_each over a flattened map of maps.
What it costs is reviewability. Anything the program computes is invisible in the pull request, so the reviewer either trusts the preview or mentally simulates the code. HCL's weakness is also its safety property: the config sits close to the plan, and what you read is roughly what will happen. In HCL the dangerous change is usually one you can see and approve anyway; in a general-purpose language it is the one produced by a conditional two modules deep that nobody rendered before merging.
Non-determinism is the specific hazard. A timestamp, a random suffix, a list fetched from an API at run time, a dependency that moved a minor version: any of them makes today's preview differ from yesterday's for reasons unconnected to intent. Pin dependencies, commit lockfiles, and treat "the preview changed and nobody edited the code" as an incident, not a quirk.
My view: if your infrastructure has genuine abstraction to express, typically a platform team publishing a primitive many teams consume, a real language pays for itself. If it is a few hundred resources described once and edited rarely, HCL's dullness is the feature you want.
What actually goes wrong
These are the failures that recur, not the exotic ones.
Coverage, and the boring half of the estate
Compute is the part people design for. The rest of an estate is DNS, certificates, CDN rules, identity providers, database roles and grants, monitoring, alert routing, storage lifecycle and CI secrets. That is where the provider question gets settled.
Terraform's provider ecosystem is the broadest of the three, and that, rather than HCL, is the substantive reason it keeps winning defaults. Pulumi inherits most of it through the bridge. CDK natively covers what CloudFormation covers, and CloudFormation has repeatedly lagged new AWS capabilities. AWS has worked to close that gap with Cloud Control API backed resources, so check the resource you need rather than assuming either way.
The escape hatch matters here. When CDK cannot express something the answer is a custom resource, a Lambda you then own, version, monitor and debug at exactly the moment you want fewer moving parts. Never free, and never in the comparison tables.
Licences, and who you are depending on
Terraform's licence changed in 2023 from the Mozilla Public Licence to the Business Source License, which is not an open-source licence. OpenTofu forked from the last MPL release and is developed under the Linux Foundation. If it matters to you or your customers that the toolchain stays open, decide that deliberately now rather than during a procurement review. HashiCorp has since been acquired by IBM, which changes nothing technically today and does change the shape of the organisation you are betting on.
Pulumi's engine and SDKs are open source, and the default path routes state and access control through its hosted service. You can keep state in your own bucket instead. If a third party in your deployment path is a problem for your posture, configure the self-managed backend at the start and exercise it, rather than planning to migrate later.
CDK is Apache licensed and costs nothing to use. The dependency is not the licence, it is the control plane: you cannot run CloudFormation yourself, cannot patch it, and cannot debug past the error string it returns.
If it runs on hardware you own
If the target is your own machines, CDK is out by construction: there is no CloudFormation to synthesise to. Terraform and Pulumi both work against hypervisor, Kubernetes, DNS and certificate providers. Both share one limit. They are good at bringing a resource into existence and poor at making the inside of a machine correct.
The remote-exec and local-exec escape hatches are where these projects go to die. They run once, they are not idempotent in the way you assume, and their failures do not map onto the state model, so a half-configured host reads as successfully created. Keep provisioning and configuration in separate tools with separate state, even when that means two pipelines.
State on one engineer's workstation is not a strategy, not even for a single-operator estate. Put it somewhere that survives the laptop and restore it from backup once, so you know the restore works. An untested backup of a state file is the most expensive assumption in this category.
If you already have an estate in one of these and it works, migrating is almost never the best use of a quarter; better state boundaries and module interfaces buy more than new syntax. For a new decision, my defaults: AWS only, one team owning its own infrastructure and wanting a single language, pick CDK, accept CloudFormation's recovery model, and set retain policies on every stateful resource before the first deploy. More than one provider, or infrastructure that will outlive the team that wrote it, pick Terraform or OpenTofu and enjoy how dull it is. A platform team building primitives other teams consume, with tests that mean something, pick Pulumi. Then spend the first week on the three things that decide how bad your worst day is: where state lives and who can read it, where the stack boundaries fall, and a recovery procedure written down and rehearsed once. The language only decides how pleasant your Tuesday is.