Infrastructure as Code & Cloud Automation

Terraform vs Pulumi vs CloudFormation: Choosing an IaC Tool

MatterAI
MatterAI
13 min read·

Terraform vs Pulumi vs CloudFormation: Choosing an IaC Tool

Terraform, Pulumi, and CloudFormation all manage infrastructure as code, but they differ in three decisive ways: the language you write in, how state is managed, and how deeply they integrate with their cloud. This guide compares them on those dimensions and gives you a decision framework.

The Three Approaches

Terraform: declarative HCL (HashiCorp Configuration Language), state stored in backends, multi-cloud via providers.

Pulumi: infrastructure in real programming languages (TypeScript, Python, Go), state in the Pulumi service or self-hosted.

CloudFormation: AWS-native, YAML/JSON templates, state managed by AWS itself.

Language: The First Decision

# Terraform — HCL, declarative
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"

  tags = {
    Name = "web-server"
  }
}
// Pulumi — TypeScript, imperative with declarative resources
import * as aws from "@pulumi/aws";

const web = new aws.ec2.Instance("web", {
  ami: "ami-0c55b159cbfafe1f0",
  instanceType: "t3.micro",
  tags: { Name: "web-server" },
});

export const publicIp = web.publicIp;
# CloudFormation — YAML, declarative
Resources:
  WebInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0c55b159cbfafe1f0
      InstanceType: t3.micro
      Tags:
        - Key: Name
          Value: web-server

The language choice is not cosmetic. Pulumi's real-language model enables loops, conditionals, and functions without HCL's awkward expressions:

// Pulumi: real loops and conditionals
const instances = environments.map(
  (env) =>
    new aws.ec2.Instance(`web-${env}`, {
      instanceType: env === "prod" ? "m5.large" : "t3.micro",
      tags: { Environment: env },
    }),
);

Terraform can do this with for_each and count, but the syntax is harder to read and test. CloudFormation has no loops at all — you generate templates or repeat blocks.

State Management

State is the source of truth for what exists in your cloud.

Terraform: state is a JSON file in a backend (S3, Terraform Cloud, local). You must configure locking and remote storage yourself. State corruption or drift is a real operational concern.

# backend.tf — remote state with locking
terraform {
  backend "s3" {
    bucket         = "my-tf-state"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "tf-locks"
  }
}

Pulumi: state in the Pulumi Cloud service (or self-hosted), with built-in locking, history, and policy. Less to configure, but a dependency on the service.

CloudFormation: AWS manages state entirely. No state files, no locking, no drift files. This is CloudFormation's biggest operational advantage: the state problem is solved for you.

Multi-Cloud

ToolMulti-CloudProvider Model
TerraformExcellent1000+ providers, community
PulumiExcellentNative providers per cloud
CloudFormationAWS onlyN/A

If you run AWS + GCP + Azure, Terraform or Pulumi are the only real options. CloudFormation locks you to AWS.

Drift and Diffing

All three detect drift between declared and actual state, but the experience differs:

  • Terraform: terraform plan shows a precise diff. Drift detection requires terraform refresh or plan runs.
  • Pulumi: pulumi preview shows a diff with the same precision, plus the ability to write tests against the plan.
  • CloudFormation: drift detection is a separate feature you must enable per stack; diffs are less granular.

Testing Infrastructure Code

This is where Pulumi's language model shines:

// Pulumi: unit-test your infrastructure like application code
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

pulumi.runtime.setMocks({
  newResource: (args) => ({ id: `${args.name}_id`, state: args.inputs }),
  call: (args) => args.inputs,
});

it("tags all instances with the environment", async () => {
  const infra = await import("./infra");
  const instance = await infra.web.instance;
  expect(instance.tags.Environment).toBe("prod");
});

Terraform has terraform test and third-party tools (Terratest), but testing HCL is inherently harder than testing TypeScript.

Comparison Table

AspectTerraformPulumiCloudFormation
LanguageHCLTS/Python/Go/etcYAML/JSON
StateBackend (you manage)Pulumi serviceAWS-managed
Multi-cloudYesYesNo
Loops/conditionalsAwkwardNativeNone
TestingLimitedStrongLimited
EcosystemLargestGrowingAWS-native
Learning curveModerateModerateLow (AWS teams)

Decision Guide

Your situationChoose
Multi-cloud, largest ecosystemTerraform
Infrastructure as real code, testingPulumi
AWS-only, want zero state managementCloudFormation
Team already knows HCLTerraform
Team is TypeScript/Python-firstPulumi
AWS CDK alternative (real language, AWS)CDK (not listed)

Implementation Checklist

  • Decide the language: HCL, real code, or YAML
  • Check multi-cloud requirements first
  • Evaluate state management: who owns the state problem?
  • Prototype loops and conditionals you actually need
  • Test the diff/plan experience on your team
  • Consider testing requirements for infrastructure
  • Plan the migration path from any existing tooling

MatterAI builds frontier AI infrastructure for engineering teams — from inference-optimized models to autonomous coding agents and agentic code reviews.

Explore what we're building:

  • Orbital IDE — Autonomous AI coding agent with background agents and deep codebase memory
  • AI Code Reviews — Agentic pre-commit reviews across GitHub, GitLab, and Bitbucket
  • Axon Models — Frontier-grade reasoning models at 70% lower inference cost

Get started free - https://app.matterai.so


Follow us on X · LinkedIn · GitHub

Share this Guide:

Ship Faster. Ship Safer.

Join thousands of engineering teams using MatterAI to autonomously build, review, and deploy code with enterprise-grade precision.

No credit card requiredSOC 2 Type IISetup in 2 min