The IaC landscape split into two philosophies about a decade ago and hasn't fully resolved the argument since. On one side: declarative configuration languages designed specifically for infrastructure (Terraform HCL, CloudFormation YAML, Bicep). On the other: general-purpose programming languages brought to infrastructure (AWS CDK, Pulumi). Both approaches have won in production at major organizations. Neither is clearly superior.
This comparison covers Terraform, AWS CDK, and Pulumi in depth — how they work, where they excel, where they struggle, and which makes sense for different team situations. It isn't a beginner introduction to any of these tools; if you're choosing between them for a real project, this assumes you've at least skimmed each one.
The core philosophical difference
Terraform's HCL is a purpose-built configuration language. It's not Turing-complete (no arbitrary loops, no recursion, limited conditionals). This is by design: HashiCorp's position is that infrastructure definitions should be readable, predictable, and safe to generate tooling around. When you read a .tf file, you can understand what it creates without executing anything.
CDK and Pulumi take the opposite position: the limitations of configuration languages are a tax on productive engineers. Why invent a domain-specific language when TypeScript already exists? Real programming languages have proper abstractions, test frameworks, package managers, IDE support, and a billion engineers who already know them. Infrastructure should be no different from application code.
Both positions have merit. The choice between them often comes down to who's writing the infrastructure more than which approach is technically superior.
Terraform
Terraform is the default choice for infrastructure-as-code in 2026. It works with every major cloud provider and hundreds of minor ones. The Terraform Registry has thousands of modules — reusable packages for common patterns like VPCs, EKS clusters, and RDS databases. That ecosystem is its most durable advantage.
HCL syntax is approachable. You describe what you want to exist:
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
}
resource "aws_subnet" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.cidr_block, 4, count.index)
availability_zone = var.availability_zones[count.index]
}
Terraform figures out the execution order based on the references between resources. You don't specify steps — you specify the end state.
Where Terraform struggles:
-
Dynamic resource generation:
countandfor_eachare usable but awkward. Generating 20 similar resources with slight variations requires careful HCL gymnastics. CDK and Pulumi can use aforloop. - Abstraction limitations: You can create modules, but you can't do real object-oriented composition. CDK constructs and Pulumi component resources are substantially more powerful for building internal platforms.
-
Testing: Testing Terraform is harder than it should be.
terraform validatechecks syntax;terratest(Go) orpytest-terraformcan do integration tests, but unit testing logic is awkward because HCL isn't executable without a provider. - State management complexity: As covered in depth in the Terraform state guide, managing state at scale requires careful structure and tooling.
When to choose Terraform: You need multi-cloud support, your team is a mix of application and platform engineers (not all software engineers), or you're inheriting existing Terraform infrastructure. Also: when you want the broadest hiring pool and the most community resources.
AWS CDK
CDK (Cloud Development Kit) is AWS's answer to the "write real code" camp. You write TypeScript, Python, Java, Go, or C# that constructs a tree of CDK constructs. When you run cdk synth, it compiles to CloudFormation JSON or YAML, which AWS then deploys.
The synthesis step is important: CDK is an abstraction on top of CloudFormation, not a deployment tool in itself. Under the hood, your resources become CloudFormation stacks with CloudFormation change sets. This means CDK inherits CloudFormation's deployment semantics, including its quirks — stack drift, resource replacement behavior, and the 500-resource-per-stack limit.
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
const vpc = new ec2.Vpc(this, 'AppVpc', {
maxAzs: 3,
natGateways: 1
});
const cluster = new ecs.Cluster(this, 'AppCluster', {
vpc,
containerInsights: true
});
// L2 constructs handle security groups, IAM roles,
// log groups, and other boilerplate automatically
const service = new ecs.FargateService(this, 'AppService', {
cluster,
taskDefinition: taskDef,
desiredCount: 3
});
CDK's L2 constructs are its killer feature. An L2 construct is an opinionated abstraction over one or more CloudFormation resources that handles the boilerplate. ecs.FargateService doesn't just create a AWS::ECS::Service — it creates the service, the task definition's execution role with the right policies, the security group, and the CloudWatch log group. Getting the same result in raw CloudFormation or Terraform requires 4–6 separate resources and knowing which policies to attach.
Where CDK struggles:
- AWS-only: CDK is designed for AWS CloudFormation. Using non-AWS resources requires CDK for Terraform (CDKTF), which is a separate project with its own quirks, or mixing in other tools.
- CloudFormation limits: That 500-resource limit per stack bites real production systems. Large applications need careful stack decomposition. CloudFormation deployments also tend to be slower than Terraform — change sets take time.
- Synthesis surprises: A single CDK construct can expand to dozens of CloudFormation resources. If you're used to explicit resource definitions, the synthesized output can be surprising — and the diff on a CDK update is sometimes much larger than expected.
- CDK versioning pain: CDK 1.x to 2.x was a painful migration. The framework moves fast and breaking changes happen.
When to choose CDK: Your team is primarily software engineers who find HCL limiting, you're AWS-only, and you want L2 construct abstractions to reduce boilerplate. Also: when you're building an internal infrastructure platform that other teams consume as a library — CDK constructs compose like packages.
Pulumi
Pulumi is similar in philosophy to CDK (real programming languages for infrastructure) but different in execution. Pulumi doesn't compile to CloudFormation — it has its own deployment engine that calls provider APIs directly, similar to Terraform. It supports TypeScript, Python, Go, C#, Java, and YAML.
import * as aws from "@pulumi/aws";
const vpc = new aws.ec2.Vpc("main", {
cidrBlock: "10.0.0.0/16",
enableDnsHostnames: true,
});
const privateSubnets = availabilityZones.map((az, i) =>
new aws.ec2.Subnet(`private-${az}`, {
vpcId: vpc.id,
cidrBlock: `10.0.${i}.0/24`,
availabilityZone: az,
})
);
This is the key difference from CDK: new aws.ec2.Vpc(...) isn't creating a CloudFormation resource. It's registering a resource with the Pulumi engine, which maintains its own state (either in Pulumi Cloud or in a self-managed backend like S3). The deployment runs directly against the AWS API, like Terraform.
Pulumi's multi-cloud support is genuine — it has providers for AWS, Azure, GCP, Kubernetes, and many others, all within the same program. You can write a TypeScript program that creates an AWS VPC, deploys an Azure SQL database, and configures a Cloudflare DNS record, all in a single Pulumi stack.
Where Pulumi struggles:
- Community and ecosystem size: Pulumi's ecosystem is smaller than Terraform's. The Registry has fewer published modules. When you hit an edge case, there are fewer Stack Overflow answers and community resources.
- Language complexity can become a liability: The same expressiveness that makes Pulumi powerful also means infrastructure code can become complex. A Pulumi program that makes API calls, fetches secrets, and uses complex TypeScript generics is harder to review than equivalent Terraform HCL.
- State backend choices: Pulumi's hosted backend (Pulumi Cloud) is polished but adds a vendor dependency. The self-managed S3 backend works but lacks features like the run history and policy-as-code that Pulumi Cloud provides.
When to choose Pulumi: Your team writes TypeScript or Python daily and finds HCL genuinely limiting. You need multi-cloud in a single program. You're building Kubernetes-native infrastructure where the Pulumi Kubernetes provider is a natural fit alongside AWS/GCP resources.
Side-by-side comparison
| Dimension | Terraform | AWS CDK | Pulumi |
|---|---|---|---|
| Language | HCL (DSL) | TS, Python, Java, Go, C# | TS, Python, Go, C#, Java, YAML |
| Deployment engine | Direct provider API | CloudFormation | Direct provider API |
| Multi-cloud | Yes (native) | AWS only (CDKTF for others) | Yes (native) |
| State management | Self-managed or Terraform Cloud | CloudFormation (AWS-managed) | Self-managed or Pulumi Cloud |
| Abstractions | Modules | Constructs (L1/L2/L3) | Component resources |
| Testing | Limited unit testing | Jest + CDK assertions | Jest/pytest + Pulumi mocking |
| Ecosystem | Largest | Large (AWS-focused) | Growing |
| Learning curve | Low (for HCL) | Medium (requires knowing CDK patterns) | Low (if already know the language) |
Hybrid approaches
Real production environments often use more than one tool. Some common combinations:
Terraform for infrastructure, CDK for application stacks. Platform teams manage VPCs, RDS clusters, and shared IAM with Terraform. Application teams deploy their ECS services and Lambda functions with CDK. The CDK stacks reference Terraform outputs via SSM parameters or remote state. This splits ownership cleanly and lets each team use the tool that fits their skillset.
Terraform for everything except Kubernetes. Terraform creates the EKS cluster. Pulumi or Helm manages Kubernetes resources. Terraform's Kubernetes provider exists but is awkward for complex K8s workloads — the HCL representation of a Kubernetes deployment is verbose and hard to read. Pulumi's Kubernetes provider or plain Helm charts are usually a better fit for the K8s layer.
CDK for core AWS, Terraform for third-party services. CDK shines for AWS resources with its L2 constructs. Terraform shines for the 1000+ non-AWS providers (Datadog, PagerDuty, Cloudflare, GitHub, Vault). Many teams use CDK for their AWS infrastructure and Terraform for everything else.
Migration considerations
If you're migrating existing infrastructure to a different IaC tool, the transition cost is often higher than it looks. Consider:
State migration: Moving from Terraform to Pulumi or CDK means re-importing every resource or doing a careful blue-green migration. There are tools to convert Terraform state to Pulumi (pulumi convert --from terraform) but they require significant manual cleanup.
Team knowledge: If your team knows Terraform, migrating to CDK doesn't make everyone immediately productive. The learning curve is real, especially the CDK mental model (constructs, stacks, apps, environments).
Partial migrations are valid: You don't have to pick one tool for everything. New projects can use the new tool. Existing infrastructure can stay where it is. The overhead of two tools is often less than the disruption of a full migration.
The version that no one talks about: OpenTofu
In 2023, HashiCorp changed Terraform's license from MPL to BSL — a non-open-source license that restricts commercial use by competing products. The community responded by forking Terraform at the last open-source version (1.5.x) to create OpenTofu, now maintained by the Linux Foundation.
OpenTofu is a drop-in replacement for Terraform with the same HCL syntax, provider compatibility, and state format. It diverges from Terraform where HashiCorp has added BSL-licensed features. For most users, OpenTofu and Terraform are functionally identical today. OpenTofu is the right choice if the license change concerns you or if you're vendor-averse; Terraform is the right choice if you want the closest alignment with the HashiCorp ecosystem and Terraform Cloud.
Whichever IaC tool you use, visualizing what it actually creates helps during both development and review. InfraSketch supports Terraform HCL, CDK synthesized JSON, and Pulumi TypeScript/Python — paste your code to see the architecture diagram without deploying anything.
Top comments (0)