dev-tools

Infrastructure as Code: Terraform vs Pulumi vs CloudFormation

Infrastructure code on screen with cloud deployment pipeline

The first time I tried to recreate a production environment by clicking through the AWS console, it took three days and I missed a security group rule that caused a four-hour outage. The second time, I wrote it in Terraform and it took 12 minutes with zero errors. That experience converted me permanently — infrastructure that isn't code is infrastructure that's one mistake away from disaster.

But which IaC tool? Terraform dominates the conversation, Pulumi challenges with real programming languages, and CloudFormation remains the native AWS option that can't be ignored. Each has genuine strengths and genuine frustrations that only emerge after months of production use.

The Core Philosophical Difference

The fundamental split in IaC is between domain-specific languages (DSLs) and general-purpose programming languages (GPLs). Terraform uses HCL (HashiCorp Configuration Language), a DSL designed specifically for infrastructure. CloudFormation uses JSON/YAML, another DSL approach. Pulumi lets you write infrastructure in Python, TypeScript, Go, C#, or Java.

This isn't a minor syntactic preference — it shapes everything about the developer experience. DSLs are constrained by design: you can't write a for loop that makes an HTTP request in HCL. That constraint prevents some antipatterns but also creates workarounds when you need dynamic behavior. GPLs give you unlimited expressiveness but also unlimited opportunity to create unmaintainable infrastructure code.

Terraform: The Industry Standard

What Works

Terraform's ecosystem is its superpower. Providers exist for every major cloud (AWS, GCP, Azure), dozens of SaaS platforms (Datadog, PagerDuty, Cloudflare, GitHub), and infrastructure components (Kubernetes, Helm, Docker). If it has an API, someone has written a Terraform provider for it. This universality means you can manage your entire infrastructure — cloud resources, DNS, monitoring, CI/CD configuration — in one tool.

HCL is readable even for people who don't write Terraform. A resource block declaring an EC2 instance is self-documenting: the resource type, the AMI, the instance type, and the tags are all visible and understandable. This readability matters enormously for code review and onboarding.

The plan/apply workflow is Terraform's best feature. terraform plan shows exactly what will change before anything changes. In a world where a wrong API call can delete a production database, this preview step is invaluable. Teams build their entire deployment confidence around the plan output.

Modules enable reuse. A well-designed module for a standard application stack (ALB + ECS + RDS + CloudWatch) can be parameterized and reused across dozens of projects. The Terraform Registry hosts thousands of community modules, though quality varies dramatically — always read the source code.

What Doesn't

State management is Terraform's original sin. The state file — a JSON document mapping your Terraform configuration to real-world resources — is simultaneously critical and fragile. Lose it and Terraform loses track of everything. Corrupt it and you're manually editing JSON or importing resources one by one. Remote state backends (S3, Terraform Cloud, GCS) mitigate the risk but add infrastructure to manage your infrastructure.

HCL's limitations surface at scale. Dynamic resource creation, conditional logic, and complex data transformations require workarounds (for_each, dynamic blocks, locals with ternary expressions) that feel like fighting the language. When you find yourself writing a for expression nested inside a dynamic block inside a for_each resource, you're solving a programming problem with a configuration language. That's where Pulumi's approach starts looking appealing.

The licensing change (BSL from MPL) in 2023 created uncertainty. OpenTofu emerged as the open-source fork. In 2026, both Terraform and OpenTofu are viable, but the ecosystem split means evaluating provider compatibility and community support for each. Most teams stay on Terraform for pragmatic reasons — existing modules, documentation, and hiring pool.

Best For

Multi-cloud environments, teams wanting a large talent pool (Terraform skills are the most in-demand IaC skill), organizations preferring declarative configuration over imperative programming, and projects where infrastructure complexity is moderate. For teams managing cloud infrastructure, pair it with your multi-cloud strategy.

Pulumi: Programming Languages for Infrastructure

What Works

Pulumi's core insight is that infrastructure complexity often exceeds what DSLs handle gracefully. Writing infrastructure in TypeScript, Python, or Go means you get loops, conditionals, functions, classes, type checking, IDE support, testing frameworks, and package managers — the tools professional developers already know.

Consider creating 10 similar but not identical microservice deployments. In Terraform, you'd use a module with many input variables and complex conditional logic. In Pulumi (TypeScript), it's a function that takes parameters and returns resources — standard programming that any developer can read, test, and refactor.

Testing is where Pulumi genuinely excels. Unit tests verify that your infrastructure code generates the expected resources with the expected properties — without deploying anything. Integration tests deploy to a real cloud and verify the result. Try unit testing a Terraform module; the tooling exists (Terratest) but feels bolted-on compared to Pulumi's native testing support.

Pulumi AI generates infrastructure code from natural language descriptions. "Create an AWS VPC with two public subnets, two private subnets, a NAT gateway, and an internet gateway" generates working Pulumi code. The quality is surprisingly good for standard patterns and saves significant scaffolding time.

What Doesn't

The talent pool is smaller. Terraform has become the IaC lingua franca — most cloud engineers know it. Pulumi requires both cloud knowledge AND proficiency in the chosen programming language. Hiring is harder. Onboarding is longer. Knowledge transfer when engineers leave is more complex.

The "too much flexibility" problem is real. Pulumi doesn't stop you from writing a 2,000-line infrastructure function with side effects, API calls, and complex state manipulation. Without disciplined coding standards, Pulumi projects can become unmaintainable faster than Terraform projects because there are fewer guardrails.

Provider coverage, while extensive, lags behind Terraform slightly. The Pulumi-Terraform bridge allows using any Terraform provider from Pulumi, which closes most gaps, but bridged providers sometimes have rough edges in documentation and type definitions.

Best For

Software development teams where developers manage infrastructure, complex infrastructure patterns that strain HCL, organizations that value testing and type safety, and teams that want to use their existing programming language skills. See our serverless computing guide for a common deployment target.

CloudFormation: The Native AWS Option

What Works

CloudFormation is AWS. New AWS services and features are available in CloudFormation on launch day — Terraform providers often lag by days to weeks. If you need the latest AWS capability immediately, CloudFormation is the only guaranteed option.

No state management headaches. CloudFormation manages state internally — there's no state file to lose, corrupt, or secure. Stack drift detection tells you when resources have been modified outside of CloudFormation. This simplicity is underrated.

StackSets deploy the same template across multiple AWS accounts and regions simultaneously. For organizations managing dozens of accounts through AWS Organizations, StackSets + Service Control Policies provide a governance model that third-party tools struggle to match.

CDK (Cloud Development Kit) bridges the gap between CloudFormation and programming languages. Write infrastructure in TypeScript, Python, Java, C#, or Go, and CDK synthesizes it to CloudFormation templates. You get Pulumi-like developer experience with CloudFormation's native integration. In many ways, CDK gives you the best of both worlds for AWS-only environments.

What Doesn't

AWS only. CloudFormation manages AWS resources and nothing else. Your DNS is on Cloudflare? Your monitoring is on Datadog? Your CI/CD is on GitHub? CloudFormation can't help. You'll need Terraform, Pulumi, or manual configuration for everything outside AWS.

Error messages are notoriously unhelpful. "Resource creation cancelled" or "Internal Failure" with no additional context is a regular CloudFormation experience. Debugging a failed stack update often means reading CloudTrail logs to figure out what API call actually failed and why.

Rollback behavior can be aggressive. A single resource failure during a stack update triggers a full rollback of all changes. For large stacks with many resources, a rollback can take 30+ minutes — during which your stack is in an UPDATE_ROLLBACK_IN_PROGRESS state and you can't make any changes. This makes iterative development painful.

Best For

AWS-only organizations, teams wanting zero state management overhead, environments where new AWS feature availability is critical, and organizations already invested in CDK.

Comparison Table

FeatureTerraformPulumiCloudFormation
LanguageHCLTypeScript, Python, Go, C#, JavaJSON/YAML (or CDK languages)
Multi-cloudYes (excellent)Yes (good)AWS only
State ManagementExternal (S3, TFC, etc.)Pulumi Cloud or self-managedManaged by AWS
Preview Changesterraform planpulumi previewChange sets
Unit TestingTerratest (external)Native (any test framework)cfn-lint, TaskCat
Learning CurveMedium (learn HCL)Low (if you know the language)Medium-High (YAML complexity)
Hiring PoolLargeGrowingMedium (AWS-focused)
Provider Ecosystem3,000+ providers100+ native + TF bridgeAWS services only
LicenseBSL 1.1 (OpenTofu: MPL)Apache 2.0Proprietary (free)

Migration Paths

CloudFormation to Terraform: Use cf2tf for automated conversion of templates. Import existing resources with terraform import. Budget 1-2 weeks per major stack. The biggest challenge is mapping CloudFormation's intrinsic functions to HCL equivalents.

Terraform to Pulumi: Pulumi provides pulumi convert --from terraform which handles most conversions. Remaining manual work is typically around complex HCL expressions that need translation to the target language's idioms. The state can be imported from Terraform state files.

Any to CDK: If you're AWS-only, CDK import can adopt existing resources. The migration is more about rewriting than converting — CDK's programming model is different enough that automated conversion produces awkward code. Worth it if you're committing to CDK long-term.

FAQ

Should I use Terraform or OpenTofu?

For new projects in 2026, both are viable. Terraform has the larger ecosystem, more documentation, and Terraform Cloud. OpenTofu has an open-source license and growing community support. If licensing terms matter to your organization, choose OpenTofu. If pragmatism and ecosystem maturity matter more, choose Terraform. Both are functionally equivalent for most use cases.

Is Pulumi worth learning if I already know Terraform?

If your infrastructure complexity is exceeding what HCL handles comfortably — lots of conditional logic, dynamic resource generation, complex data transformations — yes. If your Terraform is straightforward and maintainable, the switching cost isn't justified. Pulumi shines at the upper end of infrastructure complexity.

Can I use multiple IaC tools together?

Yes, and some teams do. A common pattern: CloudFormation or CDK for core AWS infrastructure, Terraform for multi-cloud resources and SaaS configuration. The risk is ownership ambiguity — if two tools manage resources in the same account, conflicts and drift are likely. Clear boundaries (Terraform owns networking, CDK owns application infrastructure) help.

What's the best way to structure a Terraform project?

Separate state files per environment (dev, staging, prod) and per major component (networking, compute, data). Use modules for reusable patterns. Keep root modules small — they should compose modules, not define resources directly. Use workspaces sparingly (they share state backend configuration, which creates operational risk). A DevOps-first approach to project structure pays dividends at scale.