Skip to content

Infrastructure as Code: Terraform vs Pulumi vs CDK

A
abemon
| | 12 min read | Written by practitioners
Share

The decision that defines your operations

The Infrastructure as Code (IaC) tool you choose conditions how your team thinks about infrastructure for the next 3-5 years. This is not a trivial decision. Terraform, Pulumi, and AWS CDK represent three different philosophies about how to define and manage cloud resources, and each has deep implications for team productivity, cross-provider portability, and testing capability.

We have used all three in production. This article documents what we have learned, with concrete data and honest opinions.

Terraform: the industry standard

HashiCorp’s Terraform has been around since 2014 and holds a dominant position. According to the CNCF 2024 survey, 64% of organizations using IaC choose Terraform. That critical mass matters: more documentation, more community modules, more engineers who know it.

The language: HCL

Terraform uses HCL (HashiCorp Configuration Language), a declarative language designed specifically for defining infrastructure. It is not a general-purpose programming language. It is a configuration language on steroids.

resource "aws_lambda_function" "api" {
  function_name = "api-handler"
  runtime       = "nodejs20.x"
  handler       = "index.handler"
  memory_size   = 256
  timeout       = 30

  environment {
    variables = {
      DB_HOST = aws_rds_cluster.main.endpoint
    }
  }
}

The upside of HCL: it is readable by anyone who has seen a configuration file. You do not need programming skills to understand what a Terraform plan does. The entry barrier is genuinely low. Junior engineers learn it in days, not weeks.

The downside of HCL: when you need complex logic (conditional loops, data transformations, dynamic composition), HCL becomes painfully limited. The for_each, count, dynamic blocks, and built-in functions cover many cases, but sometimes you end up fighting the language instead of expressing your intent.

A real example: generating security rules for 15 microservices where each has different ports and CIDRs. In Python, it is a loop with a dictionary. In HCL, it is a nested for_each with flatten and lookup that requires three readings to understand.

State and plan

Terraform’s model revolves around a state file that represents infrastructure reality and a plan that shows changes before applying them. terraform plan is arguably the most valuable feature of the entire tool: seeing exactly what will change before it changes.

State is also the biggest source of pain. State conflicts when two people modify infrastructure simultaneously, drift between state and reality, state mv and state rm operations that require surgical precision. Terraform Cloud and remote backends with locking mitigate the problem, but do not eliminate it.

Ecosystem

Terraform has providers for virtually everything: AWS, Azure, GCP, Cloudflare, Datadog, PagerDuty, GitHub, Kubernetes, and hundreds more. The Terraform Registry hosts over 3,500 providers and 15,000 modules. For any mainstream cloud service, there is a mature, well-maintained provider.

Community modules are a double-edged sword. Official modules (like terraform-aws-modules/vpc) are excellent. Random third-party modules vary enormously in quality. Our rule: use official or verified-organization modules; for everything else, write your own.

The license question

In August 2023, HashiCorp changed Terraform from MPL to BSL (Business Source License). This sparked the creation of OpenTofu, a fork maintained by the Linux Foundation. In practical terms, functionality is identical and the transition between the two is trivial. But if licensing is a concern for your organization, OpenTofu is the direct alternative.

Pulumi: IaC in your language

Pulumi made a radical design decision: instead of inventing a new language, it lets you define infrastructure in existing programming languages. TypeScript, Python, Go, C#, Java. Infrastructure is code, literally.

const api = new aws.lambda.Function("api", {
  runtime: "nodejs20.x",
  handler: "index.handler",
  memorySize: 256,
  timeout: 30,
  environment: {
    variables: {
      DB_HOST: cluster.endpoint,
    },
  },
});

The promise and the reality

The promise: use the tools you already know. IDEs with autocomplete, type checking, debugging, native testing frameworks. Complex logic with loops, functions, classes. Code reuse through language mechanisms rather than tool-specific modules.

The reality: the promise delivers, but with nuances. Autocomplete works and is genuinely helpful. Types prevent errors that in Terraform you would only discover at apply. But the learning curve is steeper. An engineer who knows Python still needs to learn Pulumi’s resource model, the concept of stacks, the difference between inputs and outputs, and how async dependency resolution works. It is not trivial.

Where Pulumi shines

Complex logic: when infrastructure involves significant conditional logic, Pulumi is clearly superior. Generating resources based on dynamic configuration, conditionally applying policies, transforming input data — all natural in a programming language and forced in HCL.

Testing: Pulumi enables real unit testing of infrastructure with standard frameworks (Jest, pytest, Go testing). You can mock providers and verify that your logic generates the correct resources without applying anything. In Terraform, native testing (terraform test) is functional but more limited.

Component resources: Pulumi allows creating high-level abstractions that encapsulate multiple resources. A DatabaseCluster that internally creates the RDS instance, security groups, parameter groups, and CloudWatch alerts. The component mechanism is more natural than Terraform modules because it uses language-level inheritance and composition.

Where Pulumi struggles

Smaller ecosystem: fewer providers, fewer examples, fewer engineers who know it. Finding an engineer with Pulumi experience is significantly harder than finding one with Terraform experience.

Accidental complexity: the freedom to use a general-purpose language is also an invitation to over-engineer. We have seen Pulumi projects where someone built a three-layer abstraction framework on top of basic resources. The result was harder to understand than the equivalent HCL.

State: Pulumi has its own state system, similar to Terraform’s but with Pulumi Cloud as the default backend. You can use local backends or S3, but the experience is optimized for Pulumi’s cloud service. Vendor dependency in the infrastructure management tool: ironic.

AWS CDK: Amazon’s bet

AWS Cloud Development Kit (CDK) uses the same principle as Pulumi (IaC in real languages) but with a twist: it generates CloudFormation templates as an intermediate step. You define infrastructure in TypeScript, Python, Java, C#, or Go, and CDK synthesizes it into CloudFormation JSON/YAML that AWS deploys.

const fn = new lambda.Function(this, 'ApiHandler', {
  runtime: lambda.Runtime.NODEJS_20_X,
  handler: 'index.handler',
  memorySize: 256,
  timeout: Duration.seconds(30),
  environment: {
    DB_HOST: cluster.clusterEndpoint.hostname,
  },
});

Constructs: the killer feature

CDK organizes resources into three abstraction levels called constructs:

L1 (Cfn resources): 1:1 mapping with CloudFormation resources. Verbose but complete.

L2 (curated constructs): high-level abstractions with sensible defaults. An L2 Function automatically configures the IAM role, CloudWatch logs, and retry policy. Dramatically reduces boilerplate.

L3 (patterns): predefined resource combinations. LambdaRestApi creates a Lambda, API Gateway, necessary permissions, and deployment stages in a single line.

L2 and L3 constructs are the reason CDK is so productive for teams working exclusively on AWS. A developer can stand up a complete serverless architecture in 50 lines of TypeScript. The Terraform equivalent would be 200+ lines of HCL.

The fundamental limitation

CDK is AWS only. No support for Azure, GCP, or any other provider. CDK for Terraform (CDKTF) exists as a bridge, but in practice it is a wrapper around Terraform providers with an experience inferior to native CDK.

If your infrastructure is 100% AWS with no plans to change, CDK is probably the most productive option. If there is any possibility of multi-cloud, CDK locks you to a single provider.

CloudFormation as backend

CDK generating CloudFormation is both an advantage and a limitation. Advantage: CloudFormation is a managed AWS service with automatic rollback, drift detection, and stack policies. No state to manage manually. Limitation: CloudFormation has limits (500 resources per stack, deployment speed dependent on the service), and CloudFormation errors are notoriously painful to diagnose. When CDK fails, you must read CloudFormation errors and mentally map them back to TypeScript code. Not fun.

Head-to-head comparison

CriterionTerraformPulumiCDK
LanguageHCLTS, Python, Go, C#, JavaTS, Python, Go, C#, Java
Multi-cloudExcellentGoodAWS only
Learning curveLowMedium-highMedium
TestingBasic (terraform test)Native (Jest, pytest)Native + CDK assertions
EcosystemVery largeMediumLarge (AWS)
StateSelf-managed + CloudPulumi Cloud + selfCloudFormation (managed)
MaturityHigh (10+ years)Medium (6+ years)Medium (6+ years)
HiringEasyDifficultMedium

When to choose each

Choose Terraform if: your team is operations-first, you work with multiple clouds, your staff includes people without strong programming backgrounds, or you value the largest community and ecosystem.

Choose Pulumi if: your team is development-first, you need complex infrastructure logic, you prioritize testing and type safety, and you have engineers with solid TypeScript or Python experience.

Choose CDK if: your infrastructure is 100% AWS, you want maximum productivity with sensible defaults, and you are not concerned about vendor lock-in with Amazon.

Our position: for most of our clients (mid-sized European companies with moderate multi-cloud needs and mixed operations-development teams), Terraform remains the default recommendation. Not because it is the best on any single axis, but because it is the most balanced: powerful enough, widely known, and with the most mature ecosystem. Pulumi is our second choice for teams with strong development culture that prioritize type safety and testing.

The tool matters less than you think

A confession: we have spent more time debating which IaC tool to use than we would have saved by picking the “perfect” one. All three tools work. All three have limitations. What truly determines IaC success is not the tool but the practices:

  • Everything in code: zero manual resources. If someone creates a security group from the console, it gets deleted.
  • Infrastructure review as code review: Terraform plans (or equivalents) are reviewed in PRs before applying.
  • Reusable modules: do not copy and paste. Encapsulate common patterns in modules or components.
  • Identical environments: staging must be a replica of production, defined with the same code.
  • Drift detection: detect and correct manual changes periodically.

Pick a tool, learn to use it well, and focus your energy on practices, not the next tool.

Our cloud and DevOps team implements infrastructure as code for organizations of all sizes. If you are considering an IaC migration or starting from scratch, we can help you choose the right tool and establish the practices that make it work. For multi-cloud architectures, the IaC choice is especially critical.

About the author

A

abemon engineering

Engineering team

Multidisciplinary engineering, data and AI team headquartered in the Canary Islands. We build, deploy and operate custom software solutions for companies at any scale.