DevOps

Infrastructure as Code: Terraform vs. Pulumi vs. CDK — Which to Choose and Why

DSi
DSi Team
· · 12 min read
Infrastructure as Code: Terraform vs. Pulumi vs. CDK

Infrastructure as code is no longer optional. Today, any engineering team managing cloud resources manually through a web console is accumulating technical debt that compounds with every deployment. The question is not whether to adopt IaC, but which tool to standardize on — and that decision has real consequences for your team's velocity, your cloud costs, and your ability to scale.

Three tools dominate the IaC landscape: HashiCorp Terraform, Pulumi, and AWS CDK. Each takes a fundamentally different approach to the same problem. Terraform uses a purpose-built declarative language. Pulumi lets you write infrastructure in general-purpose programming languages. CDK gives you high-level abstractions that compile down to CloudFormation. They all work. They all have trade-offs. And picking the wrong one for your team can cost you months of productivity.

This guide breaks down what each tool actually does, how they compare across the dimensions that matter, and how to decide which one fits your organization. If you are a CTO evaluating IaC tools, a platform engineer building internal tooling, or a DevOps team working toward operational maturity, this is the comparison you need.

What Infrastructure as Code Actually Solves

Before comparing tools, it is worth grounding this discussion in the problem they solve. Infrastructure as code means defining your cloud resources — servers, databases, networking, IAM policies, DNS records, everything — in version-controlled files that are the single source of truth for your environment.

Without IaC, you end up with environments that drift apart. Staging does not match production because someone made a manual change three months ago that no one documented. Disaster recovery is a prayer, not a process. Onboarding a new engineer means walking them through a labyrinth of console settings that exist nowhere in code. And auditing who changed what and when is effectively impossible.

IaC eliminates these problems by making infrastructure reproducible, reviewable, and automated. You write code that describes your desired state, the tool figures out what changes to make, and you review those changes in a pull request before applying them. The same workflow your application code follows — code review, CI/CD, automated testing — now applies to your infrastructure.

The core principles are shared across all three tools:

  • Declarative intent: You describe what you want, not the step-by-step commands to create it. The tool handles the ordering, dependencies, and API calls.
  • State tracking: The tool maintains a record of what it has created, so it can calculate the difference between the current state and your desired state.
  • Idempotency: Running the same code twice produces the same result. No duplicate resources, no unintended side effects.
  • Version control: Infrastructure definitions live in Git, giving you history, branching, collaboration, and rollback capabilities.

Where the tools diverge is in how they implement these principles — and those implementation differences matter enormously in practice.

Terraform: The Industry Standard

Terraform, maintained by HashiCorp, has been the default IaC tool since it reached maturity around 2017. It uses HCL (HashiCorp Configuration Language), a purpose-built declarative language designed specifically for defining infrastructure.

How Terraform works

You write .tf files that declare resources, data sources, variables, and outputs. Terraform reads these files, builds a dependency graph, and generates an execution plan that shows exactly what it will create, modify, or destroy. You review the plan and then apply it.

Terraform's state file is a JSON document that maps your declared resources to real cloud resources. This state is the tool's memory — without it, Terraform does not know what already exists. State can be stored locally (never do this in production) or in a remote backend like S3, Terraform Cloud, or a database.

Strengths

  • Multi-cloud by design: Terraform has providers for AWS, Azure, GCP, Kubernetes, Datadog, PagerDuty, GitHub, and hundreds more. If a service has an API, there is likely a Terraform provider for it. This makes it the natural choice for teams managing resources across multiple clouds or SaaS platforms.
  • Massive ecosystem: The Terraform Registry hosts thousands of community modules — pre-built, reusable infrastructure patterns. Need a VPC with best-practice subnet layouts? A module exists. Need an EKS cluster with managed node groups? There is a module for that too.
  • Predictable execution: HCL is intentionally limited. It does not have loops, conditionals, or abstractions that can produce surprising behavior at scale. What you read in the code is what gets applied. This predictability is a feature, not a limitation, for platform teams managing critical infrastructure.
  • Mature tooling: Terraform has years of tooling built around it — Terragrunt for managing multiple environments, Checkov and tfsec for security scanning, Spacelift and Env0 for managed execution, and Atlantis for pull-request-based workflows.

Limitations

  • HCL is its own language: Your engineers need to learn HCL, which is not a transferable skill. For simple infrastructure it is approachable, but complex logic — dynamic blocks, conditional resource creation, string manipulation — becomes unwieldy.
  • State management complexity: State splitting, state locking, state migration, and dealing with corrupted state files are operational burdens that every Terraform team encounters eventually. State is both Terraform's superpower and its Achilles heel.
  • Testing is bolted on: Terraform's built-in testing framework (terraform test) was added relatively late. Testing infrastructure code in Terraform still requires external tools like Terratest (Go-based) or writing custom validation scripts.
  • License changes: HashiCorp moved Terraform to the BSL (Business Source License) in 2023, which prompted the creation of the OpenTofu fork. Teams need to evaluate their license compliance and decide between Terraform and OpenTofu.

Pulumi: Infrastructure in Real Code

Pulumi takes a fundamentally different approach. Instead of a domain-specific language, you write infrastructure code in general-purpose programming languages — TypeScript, Python, Go, C#, Java, and YAML. Your infrastructure definitions are actual programs that run in a standard runtime.

How Pulumi works

You write a Pulumi program in your language of choice. When you run pulumi up, the Pulumi engine executes your program, intercepts the resource declarations, builds a dependency graph, and generates an execution plan — similar to Terraform's workflow. The key difference is that the resource declarations happen inside a real program, which means you have access to loops, conditionals, functions, classes, type systems, and package managers.

Pulumi also maintains state, stored either in the Pulumi Cloud service (the default) or in self-managed backends like S3, Azure Blob Storage, or a local file system.

Strengths

  • Use languages your team already knows: If your application engineers write TypeScript or Python, they can write infrastructure code in the same language without learning HCL. This removes the barrier between "application developers" and "infrastructure developers" and lets more of your team contribute to infrastructure changes.
  • Full programming language capabilities: Need to generate resources dynamically based on a configuration file? Write a loop. Need to enforce naming conventions? Write a function. Need to share infrastructure patterns across teams? Publish a package to npm or PyPI. The full power of the language ecosystem is available.
  • First-class testing: Because your infrastructure code is a standard program, you can test it with standard testing frameworks — Jest for TypeScript, pytest for Python, Go's testing package. Unit tests, integration tests, and property-based tests work exactly as they do for application code.
  • Terraform provider compatibility: Pulumi can use Terraform providers through its bridge, giving you access to nearly the same provider ecosystem without writing HCL.
  • Policy as code: Pulumi CrossGuard lets you define organizational policies (no public S3 buckets, all resources must be tagged, encryption must be enabled) as code that runs during the preview phase, before any resources are created.

Limitations

  • Complexity risk: The freedom of a general-purpose language is a double-edged sword. Teams can over-engineer their infrastructure code with deep class hierarchies, complex abstractions, and patterns borrowed from application development that make infrastructure harder to understand, not easier.
  • Smaller community: Pulumi's community and module ecosystem, while growing, is still smaller than Terraform's. You will find fewer ready-made examples and more situations where you need to write things from scratch.
  • State service dependency: The default Pulumi state backend is the Pulumi Cloud service. While self-managed backends exist, the tooling and experience are optimized for the hosted service, which is a commercial product.
  • Debugging overhead: When something goes wrong in a Pulumi program, you are debugging both your infrastructure logic and the Pulumi SDK's behavior. Stack traces can be long, and the interaction between your code and the Pulumi engine is not always transparent.

AWS CDK: High-Level Abstractions for AWS

The AWS Cloud Development Kit is Amazon's answer to infrastructure as code. Like Pulumi, it lets you write infrastructure in general-purpose languages — TypeScript, Python, Java, C#, and Go. Unlike Pulumi, it compiles your code into CloudFormation templates, which AWS then executes.

How CDK works

CDK introduces a three-tier construct system. L1 constructs are direct CloudFormation resource mappings — one-to-one with every AWS resource type. L2 constructs are opinionated, higher-level abstractions that encode AWS best practices. For example, an L2 S3 bucket construct creates the bucket with encryption enabled, public access blocked, and lifecycle policies configured by default. L3 constructs (also called patterns) combine multiple resources into complete architectures — an API Gateway backed by Lambda functions with DynamoDB tables and IAM roles, all wired together with sensible defaults.

When you run cdk synth, CDK compiles your program into a CloudFormation template. When you run cdk deploy, it submits that template to CloudFormation, which handles the actual provisioning. This means CDK inherits both CloudFormation's reliability (battle-tested, deeply integrated with AWS) and its limitations (slow rollbacks, opaque error messages, AWS-only).

Strengths

  • Highest level of abstraction: CDK's L2 and L3 constructs encode years of AWS architectural best practices. A single L3 construct can create dozens of properly configured resources — networking, security, compute, storage — with one function call. This dramatically reduces the amount of code you write and the number of AWS-specific decisions you need to make.
  • Deep AWS integration: CDK understands AWS service relationships at a level other tools do not match. It can automatically generate IAM policies based on the permissions your resources actually need, configure security groups based on resource connections, and wire up event sources between services.
  • General-purpose languages: Like Pulumi, CDK lets you use TypeScript, Python, Java, and others. You get loops, conditionals, type safety, and IDE support. CDK's TypeScript experience, in particular, is excellent — the type definitions are comprehensive and the editor auto-completion guides you through configuration options.
  • Free and open-source: CDK is fully open source (Apache 2.0 license) with no commercial licensing concerns. CloudFormation, which CDK compiles to, is a free AWS service — you pay only for the resources you provision.

Limitations

  • AWS only: CDK manages AWS resources. If you need to manage Azure, GCP, Kubernetes, or third-party SaaS services, CDK cannot help. CDK for Terraform (cdktf) exists as a separate project but has a different developer experience and a smaller community.
  • CloudFormation constraints: CDK compiles to CloudFormation, which means you inherit CloudFormation's 500-resource stack limit, its slow rollback behavior on failures, and its occasionally cryptic error messages. When a CDK deployment fails, you often need to debug at the CloudFormation layer, not the CDK layer.
  • Abstraction leakage: L2 constructs are opinionated, which is great until you need to override a default. Customizing behavior that the construct author did not anticipate can require escaping to L1 constructs or using escape hatches, which defeats the purpose of the abstraction.
  • State is CloudFormation: CDK does not manage its own state — CloudFormation does. This means stack operations go through CloudFormation's workflow, which can be slow and does not support the kind of targeted state manipulation (import, move, remove) that Terraform and Pulumi offer.

Head-to-Head Comparison

Here is how the three tools compare across the dimensions that actually matter when you are choosing one for your team.

Dimension Terraform Pulumi AWS CDK
Language HCL (domain-specific) TypeScript, Python, Go, C#, Java TypeScript, Python, Java, C#, Go
Multi-cloud Excellent (700+ providers) Good (via Terraform bridge + native) AWS only
Learning curve Moderate (new language, simple concepts) Low for app devs, moderate for ops Low for AWS-experienced teams
Testing Bolted on (Terratest, terraform test) First-class (standard test frameworks) Good (assertions, snapshot tests)
State management Self-managed or Terraform Cloud Pulumi Cloud or self-managed CloudFormation (AWS-managed)
Ecosystem maturity Largest (modules, tools, community) Growing (leverages Terraform providers) Strong for AWS (Construct Hub)
Abstraction level Low to medium (modules) Medium (component resources) High (L1/L2/L3 constructs)
Execution speed Fast (direct API calls) Fast (direct API calls) Slower (CloudFormation pipeline)
License BSL 1.1 (or OpenTofu: MPL 2.0) Apache 2.0 Apache 2.0
Best for Multi-cloud, platform teams Dev-heavy teams, complex logic AWS-only shops, rapid prototyping

Choosing the Right Tool for Your Team

The comparison table helps, but the real decision depends on your specific context. Here are the factors that should drive your choice.

Choose Terraform if:

  • You operate across multiple cloud providers or manage significant third-party SaaS infrastructure alongside your cloud resources
  • You have a dedicated platform or DevOps team that will own infrastructure code and wants a language purpose-built for the domain
  • You value the largest possible ecosystem of pre-built modules, community knowledge, and third-party tooling
  • You prefer a clear separation between infrastructure code and application code — different repositories, different deployment pipelines, different mental models
  • You want to hire DevOps engineers who likely already know the tool — Terraform is the most common IaC skill listed on resumes

Choose Pulumi if:

  • Your engineering team is primarily application developers who write TypeScript, Python, or Go and you want them to contribute directly to infrastructure code without learning a new language
  • Your infrastructure logic is complex — lots of conditional resource creation, dynamic configuration, or patterns that benefit from real programming constructs like classes, interfaces, and package management
  • Testing is a first-class requirement and you want to use the same testing frameworks and CI/CD patterns for infrastructure code that you use for application code
  • You are migrating from Terraform and want to do it incrementally using Pulumi's Terraform state import and provider bridge capabilities

Choose CDK if:

  • Your infrastructure is entirely on AWS and you have no plans or requirements to support other cloud providers
  • You want the highest level of abstraction — L2 and L3 constructs that encode AWS best practices so your team writes less code and makes fewer low-level decisions
  • Your team is already fluent in CloudFormation concepts and wants to move to a more productive authoring experience without changing the underlying deployment mechanism
  • You are building serverless or event-driven architectures on AWS where CDK's automatic IAM policy generation and service integration wiring save significant development time
The worst choice is no choice. Teams that delay IaC adoption because they cannot decide between tools end up with the most expensive outcome: infrastructure that exists only in the cloud console, understood by one or two people, and impossible to reproduce or audit. Pick a tool, commit to it, and iterate. You can always migrate later — all three tools support importing existing resources.

Testing Infrastructure Code

One of the most consequential differences between these tools is how they handle testing. Infrastructure code that is not tested is infrastructure code that will break in production — and infrastructure failures are typically more damaging than application bugs because they affect every service running on that infrastructure.

Unit testing

Unit tests validate your infrastructure logic without provisioning real resources. They answer questions like: Does this module create the right resources with the right configuration? Does the naming convention function produce correct results? Are security-sensitive defaults set correctly?

  • Terraform: The terraform test command (introduced in v1.6) supports unit testing with mock providers. It works, but the test syntax is HCL-based and less expressive than general-purpose testing frameworks. Terratest (Go) is still the most popular option for teams that want richer assertions.
  • Pulumi: Unit testing uses standard frameworks — Jest for TypeScript, pytest for Python, Go's testing package. You mock the Pulumi engine and assert against the resource properties your program creates. This feels natural for application developers.
  • CDK: The assertions module lets you inspect the synthesized CloudFormation template. Snapshot tests capture the full template and flag unintended changes. Fine-grained assertions check specific resource properties. CDK's testing story is solid and well-documented.

Integration testing

Integration tests provision real resources in an isolated environment, validate that they work, and then destroy them. These tests are slow and cost money, but they catch issues that unit tests cannot — API behavior changes, permission misconfigurations, and networking problems.

All three tools support integration testing, but Pulumi and CDK have an advantage here because their tests run in general-purpose languages. You can use HTTP clients, database drivers, and SDK calls within your test code to verify that provisioned resources actually function correctly — not just that they exist.

Policy testing

Policy tests enforce organizational rules before resources are created. Pulumi CrossGuard and tools like Open Policy Agent (OPA) with Terraform provide this capability. CDK has cdk-nag for AWS-specific compliance checks. Regardless of which tool you choose, policy testing should be part of your full development lifecycle — catching a misconfigured security group in code review is infinitely cheaper than catching it in a security audit.

State Management: The Hidden Complexity

State management is the operational burden that IaC tool comparisons often understate. Every tool needs to track what resources it has created, and managing that state is where most real-world operational pain lives.

Terraform state

Terraform's state file is a JSON document stored in a backend you configure — typically S3 with DynamoDB locking, Terraform Cloud, or a similar remote backend. You are responsible for securing it (it can contain secrets), backing it up, and handling state locking to prevent concurrent modifications. State splitting — breaking a monolithic state file into smaller, scoped state files — is a common scaling pattern that requires careful planning. When state gets corrupted or out of sync, manual intervention with terraform state commands is required, and this is one of the riskier operations in Terraform's workflow.

Pulumi state

Pulumi's default state backend is the Pulumi Cloud service, which handles encryption, locking, history, and concurrent access management out of the box. This is operationally simpler than managing your own Terraform state backend. Self-managed backends (S3, Azure Blob, local file) are available but require you to handle the same operational concerns that Terraform teams face.

CDK state

CDK delegates state management entirely to CloudFormation. Each CDK stack maps to a CloudFormation stack, and AWS manages the state. You never see a state file, never worry about locking, and never deal with state corruption. The trade-off is less control — you cannot manipulate state directly, and CloudFormation's stack operations are slower and less flexible than Terraform's state commands.

Migration Strategies

If you are already using one IaC tool and considering a switch — or if you have manually provisioned infrastructure you need to bring under IaC control — migration is a real concern. Here is a practical approach.

From manual infrastructure to IaC

  1. Inventory your resources. Use tools like AWS Config, Azure Resource Graph, or cloud-provider CLIs to generate a list of everything that exists. You will be surprised by what you find.
  2. Import, do not recreate. All three tools support importing existing resources into their state. Terraform has terraform import and the newer import block. Pulumi has pulumi import. CDK supports imports through CloudFormation resource imports. Import resources rather than destroying and recreating them to avoid downtime.
  3. Start with non-critical environments. Begin with development or staging. Get your team comfortable with the workflow before touching production infrastructure.
  4. Adopt incrementally. You do not need to import every resource on day one. Start with the resources that change most frequently — application infrastructure, deployment pipelines, feature-specific resources — and work outward toward foundational infrastructure like networking and IAM.

From Terraform to Pulumi

Pulumi provides a direct migration path from Terraform. The pulumi convert command translates HCL to Pulumi code in your language of choice. The Terraform bridge allows Pulumi to use Terraform providers natively. And Pulumi can read Terraform state files to import existing resources without re-provisioning. This makes Terraform-to-Pulumi the smoothest migration path of the three possible transitions.

From Terraform to CDK

There is no direct conversion tool from Terraform to CDK. Migration requires rewriting your infrastructure definitions in CDK and importing existing AWS resources into CloudFormation stacks. This is the most labor-intensive migration path and is only worth pursuing if you are fully committed to AWS and the CDK abstraction model provides clear benefits for your team.

Hybrid approaches

Many teams run multiple IaC tools successfully. A common pattern is using Terraform for foundational, cross-cutting infrastructure (networking, DNS, IAM) and Pulumi or CDK for application-level infrastructure that changes with each deployment. For teams heavily invested in Kubernetes, Crossplane is emerging as a Kubernetes-native alternative that lets you manage cloud resources using Kubernetes custom resources and the GitOps workflow. This is pragmatic — use the tool that fits the problem rather than forcing one tool to fit every use case.

Organizational Considerations

The technical comparison matters, but the organizational context often matters more. The best IaC tool is the one your team will actually use consistently.

Team structure

If you have a dedicated platform team that owns infrastructure, Terraform's purpose-built language and operational model work well. If you practice a "you build it, you run it" model where application teams own their infrastructure, Pulumi or CDK remove the language barrier that prevents developers from making infrastructure changes.

Hiring

Terraform skills are the most common in the market. If you need to augment your team with DevOps engineers quickly, Terraform experience is the easiest to find. Pulumi and CDK skills are growing but still less prevalent, which means hiring or contracting for those skills may take longer.

Long-term maintainability

Consider what happens when the engineer who wrote your infrastructure code leaves. HCL is constrained enough that any Terraform-literate engineer can pick it up. Pulumi and CDK codebases can develop the same complexity problems as application code — deep abstractions, clever patterns, and implicit conventions that require significant context to understand. Code review standards and documentation matter more with general-purpose language IaC tools.

Vendor independence

Terraform and Pulumi are cloud-agnostic. CDK locks you into AWS. If multi-cloud is on your roadmap — even as a future possibility — CDK as your sole IaC tool is a risky bet. If you are confidently all-in on AWS, that lock-in is a non-issue and CDK's deep AWS integration becomes a clear advantage.

Best Practices Regardless of Tool

Whichever tool you choose, these practices will determine whether your IaC implementation succeeds or becomes another source of technical debt.

  • Treat infrastructure code like application code. Code review every change. Run automated tests in CI. Use linting and formatting tools. Maintain documentation. The moment infrastructure code becomes a second-class citizen, quality degrades.
  • Separate state by blast radius. A single state file or stack for your entire infrastructure means a single mistake can affect everything. Split state by environment (dev, staging, production), by service, or by rate of change. Foundational resources that rarely change should have separate state from application resources that change daily.
  • Implement drift detection. Run plan or preview commands on a schedule (not just during deployments) to detect when someone has made a manual change that creates drift between your code and reality. Alert on drift immediately.
  • Use modules and abstractions intentionally. Shared modules are powerful for enforcing standards across teams. But over-abstraction creates the same problems in infrastructure code that it creates in application code — indirection that makes simple changes difficult. Abstract when you have proven the pattern, not preemptively.
  • Secure your state. State files often contain sensitive values — database passwords, API keys, connection strings. Encrypt state at rest, restrict access to the state backend, and use the tool's secret management features rather than storing secrets in plain text.

Conclusion

There is no universally correct answer to the Terraform vs. Pulumi vs. CDK question. Each tool reflects a different philosophy about how infrastructure should be managed, and the right choice depends on your cloud strategy, team composition, and operational maturity.

Terraform is the safe, proven choice with the largest ecosystem and the widest multi-cloud support. Pulumi is the right choice for teams that want their application developers writing infrastructure code in languages they already know. CDK is the right choice for AWS-native teams that want high-level abstractions and deep service integration without managing state themselves.

What matters more than the tool is the commitment to managing infrastructure as code in the first place. Version-controlled, reviewed, tested, and automated infrastructure is the foundation that everything else — deployment velocity, reliability, security, compliance — is built on. Pick the tool that your team will adopt consistently, invest in the practices that make it sustainable, and iterate from there.

At DSi, our DevOps engineers work across all three tools and help teams choose, implement, and scale the right IaC solution for their specific context. Whether you are adopting infrastructure as code for the first time or migrating between tools, talk to our engineering team about the right approach for your infrastructure.

FAQ

Frequently Asked
Questions

It depends on your team and cloud strategy. Choose Terraform if you need multi-cloud support, have a dedicated platform team, and value a massive ecosystem of providers and modules. Choose Pulumi if your engineers prefer writing infrastructure in TypeScript, Python, or Go and you want tight integration with application code. Choose AWS CDK if you are fully committed to AWS and want the highest level of abstraction with L2 and L3 constructs that encode AWS best practices.
Yes. Pulumi can consume existing Terraform state files and providers through its Terraform bridge, so you can migrate incrementally rather than rewriting everything at once. Some teams use Terraform for foundational infrastructure like networking and IAM while using Pulumi or CDK for application-level resources that change more frequently. This hybrid approach lets you adopt new tools without a risky big-bang migration.
Absolutely. Terraform remains the most widely adopted IaC tool in 2024 with the largest ecosystem of providers, modules, and community knowledge. Its declarative HCL syntax is purpose-built for infrastructure and avoids the complexity of general-purpose languages. The license change to BSL and the emergence of OpenTofu have added a new consideration, but the core tool and ecosystem remain strong. For multi-cloud environments and teams with dedicated DevOps or platform engineers, Terraform is often still the best choice.
Start with a non-critical environment like staging or development. Import existing resources into the new tool's state without destroying and recreating them. Migrate one service or module at a time rather than attempting a full migration at once. Run both tools in parallel during the transition period and validate that the new tool produces identical infrastructure before decommissioning the old one. Budget 2 to 6 months for a full migration depending on the size of your infrastructure.
The most common mistake is treating IaC as a one-time setup rather than a living codebase. Teams write infrastructure code to provision their initial environment and then make manual changes through the cloud console, creating drift between the code and reality. The second biggest mistake is not testing infrastructure code — treating it differently from application code when it deserves the same rigor of code review, automated testing, and CI/CD pipelines.
DSi engineering team
LET'S CONNECT
Modernize your
infrastructure automation
Our DevOps engineers bring deep IaC expertise across Terraform, Pulumi, and CDK — choose the right tool and implement it right.
Talk to the team