IaC

Terraform vs CDK vs Pulumi: Choosing Your Infrastructure-as-Code Tool

By Raghvendra Pandey · June 2026 · 10 min read

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:

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:

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:

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.

Related articles