Terraform vs CloudFormation vs Pulumi: IaC for 2026

In-depth comparison of Terraform, CloudFormation, and Pulumi for infrastructure as code in 2026. Covers syntax, multi-cloud, state, modules, and team workflows.

Terraform vs CloudFormation vs Pulumi: Infrastructure as Code Comparison for 2026

Choosing an Infrastructure as Code tool in 2026 is no longer a question of whether to adopt IaC. Every serious engineering organization has moved past manual console clicking. The real decision is which tool aligns with your team's language preferences, cloud strategy, compliance requirements, and operational maturity. This comparison draws from production deployments across AWS, Azure, and GCP to give you the data needed to make that decision.

The Three Contenders at a Glance

Dimension Terraform CloudFormation Pulumi
Vendor HashiCorp (IBM) AWS Pulumi Corporation
Language HCL (HashiCorp Configuration Language) JSON / YAML Python, TypeScript, Go, C#, Java
Multi-cloud Native multi-cloud via providers AWS only Native multi-cloud via providers
State management Remote backends (S3, Terraform Cloud, etc.) Managed by AWS Pulumi Cloud or self-managed backends
Pricing Open Source (BSL 1.1) / Terraform Cloud paid tiers Free (AWS charges for resources only) Open Source / Pulumi Cloud paid tiers
Current version Terraform 1.9.x / OpenTofu 1.8.x Latest transforms and modules (2024+) Pulumi 3.x
Learning curve Medium (new language, but declarative) Low for AWS-only teams Low for developers (familiar languages)

Language and Syntax

Terraform HCL

Terraform uses HCL, a declarative domain-specific language purpose-built for infrastructure definition. HCL sits between full programming languages and static configuration formats like YAML.

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name        = "production-vpc"
    Environment = "prod"
    ManagedBy   = "terraform"
  }
}

resource "aws_subnet" "private" {
  count             = 3
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "private-${data.aws_availability_zones.available.names[count.index]}"
    Tier = "private"
  }
}

HCL strengths: built-in functions like cidrsubnet(), lookup(), templatefile(). The count and for_each meta-arguments handle resource iteration. Variables, locals, and outputs provide clear data flow.

HCL limitations: no native looping constructs beyond count/for_each, no exception handling, limited string manipulation compared to general-purpose languages. Complex conditional logic often requires workarounds with ternary expressions and try() functions.

CloudFormation YAML/JSON

CloudFormation templates are declarative YAML or JSON documents. AWS added several features over the years to address early limitations: nested stacks, modules, transforms (macros), and the CDK layer.

AWSTemplateFormatVersion: '2010-09-09'
Description: Production VPC with private subnets

Parameters:
  VpcCidr:
    Type: String
    Default: '10.0.0.0/16'
    AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})'

Resources:
  MainVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidr
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: production-vpc
        - Key: Environment
          Value: prod

  PrivateSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MainVPC
      CidrBlock: !Select [0, !Cidr [!Ref VpcCidr, 3, 8]]
      AvailabilityZone: !Select [0, !GetAZs '']
      Tags:
        - Key: Name
          Value: !Sub 'private-${AWS::Region}a'

Outputs:
  VpcId:
    Value: !Ref MainVPC
    Export:
      Name: !Sub '${AWS::StackName}-VpcId'

CloudFormation strengths: zero additional state management (AWS handles it), deep integration with all AWS services (often day-one support for new services), native IAM integration, StackSets for multi-account/multi-region deployment, Change Sets for previewing changes before execution.

CloudFormation limitations: verbose syntax, limited intrinsic functions, no multi-cloud capability, error messages can be cryptic, rollback behavior on failure can leave stacks in UPDATE_ROLLBACK_FAILED state requiring manual intervention.

Pulumi (TypeScript Example)

Pulumi lets you write infrastructure using standard programming languages. This means full access to conditionals, loops, error handling, unit testing frameworks, and existing package ecosystems.

import * as aws from "@pulumi/aws";

const vpc = new aws.ec2.Vpc("main", {
  cidrBlock: "10.0.0.0/16",
  enableDnsHostnames: true,
  enableDnsSupport: true,
  tags: {
    Name: "production-vpc",
    Environment: "prod",
    ManagedBy: "pulumi",
  },
});

const azs = aws.getAvailabilityZones({ state: "available" });

const privateSubnets = azs.then(azs =>
  azs.names.slice(0, 3).map((az, index) =>
    new aws.ec2.Subnet(`private-${az}`, {
      vpcId: vpc.id,
      cidrBlock: `10.0.${index}.0/24`,
      availabilityZone: az,
      tags: {
        Name: `private-${az}`,
        Tier: "private",
      },
    })
  )
);

export const vpcId = vpc.id;

Pulumi strengths: full programming language capabilities (type checking, IDE autocompletion, refactoring tools), native testing with standard test frameworks (Jest, pytest, Go test), reusable components as classes/packages, Automation API for embedding Pulumi in other applications.

Pulumi limitations: the full power of a programming language means more ways to write unmaintainable code, async/Promise handling in TypeScript can trip up beginners, the learning curve for Pulumi-specific concepts (Output types, Input types, apply chains) exists even for experienced developers.

State Management

State management is where these tools diverge most significantly in operational practice.

Terraform State

Terraform stores the mapping between your configuration and real infrastructure in a state file (terraform.tfstate). This file is the source of truth for what Terraform manages.

# Backend configuration for team use
terraform {
  backend "s3" {
    bucket         = "citadel-terraform-state"
    key            = "prod/vpc/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-state-locks"
    encrypt        = true
  }
}

Critical state operations every Terraform user must know:

  • terraform state list — enumerate all managed resources
  • terraform state show aws_vpc.main — inspect a specific resource
  • terraform state mv — rename or move resources without destroy/recreate
  • terraform state rm — remove a resource from state (Terraform stops managing it, does not destroy it)
  • terraform import — bring existing infrastructure under Terraform management

State locking via DynamoDB prevents concurrent modifications. Without it, two engineers running terraform apply simultaneously can corrupt the state file.

CloudFormation State

CloudFormation manages state entirely on the AWS side. You never interact with a state file directly. This eliminates an entire category of operational concerns: no state corruption, no state file migration, no locking configuration.

The tradeoff: you have less visibility into and control over the state. Drift detection (aws cloudformation detect-stack-drift) tells you when resources have been modified outside CloudFormation, but remediation options are limited compared to Terraform's state manipulation commands.

Pulumi State

Pulumi's state model mirrors Terraform's conceptually but defaults to Pulumi Cloud as the backend, which provides built-in encryption, access controls, and a web dashboard.

# Self-managed backend options
pulumi login --local                     # Local file system
pulumi login s3://my-pulumi-state        # S3 backend
pulumi login azblob://my-pulumi-state    # Azure Blob Storage
pulumi login gs://my-pulumi-state        # GCS backend

Pulumi Cloud offers secrets encryption by default, audit logging, and team-based access controls. The self-hosted option provides the same capabilities on your own infrastructure.

Multi-Cloud Strategy

If your organization operates across AWS, Azure, and GCP, Terraform and Pulumi are the viable options. CloudFormation is definitively out.

Terraform Multi-Cloud

Terraform's provider ecosystem is its strongest multi-cloud advantage. Over 4,000 providers exist in the Terraform Registry, covering every major cloud, SaaS platform, and infrastructure component.

# Multi-cloud provider configuration
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
    google = {
      source  = "hashicorp/google"
      version = "~> 5.0"
    }
  }
}

In practice, most teams keep separate Terraform configurations per cloud rather than mixing providers in a single configuration. This keeps blast radius contained and state files manageable.

Pulumi Multi-Cloud

Pulumi achieves multi-cloud through provider packages that mirror Terraform providers (Pulumi can use Terraform providers via a bridge). The programming language layer adds the ability to create abstractions that normalize interfaces across clouds.

// Abstraction over cloud-specific storage
function createObjectStorage(cloud: "aws" | "azure" | "gcp", name: string) {
  switch (cloud) {
    case "aws":
      return new aws.s3.Bucket(name, { /* ... */ });
    case "azure":
      return new azure.storage.Account(name, { /* ... */ });
    case "gcp":
      return new gcp.storage.Bucket(name, { /* ... */ });
  }
}

This abstraction pattern is powerful but requires careful design to avoid leaky abstractions that hide cloud-specific behavior you actually need to control.

Module and Reusability Patterns

Terraform Modules

Terraform modules are directories of .tf files that accept inputs and produce outputs. The Terraform Registry hosts thousands of community modules.

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.5.0"

  name = "production"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway = true
  single_nat_gateway = false

  tags = {
    Terraform   = "true"
    Environment = "prod"
  }
}

Module versioning and the registry provide a mature ecosystem for sharing and reusing infrastructure patterns. Internal module registries (via Terraform Cloud or Artifactory) enable organization-wide standardization.

CloudFormation Modules and Nested Stacks

CloudFormation offers two reuse mechanisms: nested stacks (templates referencing other templates) and modules (packaged templates registered in the CloudFormation Registry).

# Using a nested stack
Resources:
  VPCStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://s3.amazonaws.com/cf-templates/vpc-template.yaml
      Parameters:
        VpcCidr: '10.0.0.0/16'
        Environment: prod

CloudFormation modules allow you to define reusable resource patterns and register them as custom types (MyOrg::Network::VPC). This is less mature than Terraform's module ecosystem but provides similar functionality for AWS-only environments.

Pulumi Component Resources

Pulumi's reuse model leverages standard package management (npm, PyPI, NuGet, Go modules). Component resources are classes that encapsulate multiple resources.

class VpcComponent extends pulumi.ComponentResource {
  public readonly vpcId: pulumi.Output<string>;
  public readonly privateSubnetIds: pulumi.Output<string>[];

  constructor(name: string, args: VpcArgs, opts?: pulumi.ComponentResourceOptions) {
    super("citadel:network:Vpc", name, {}, opts);

    const vpc = new aws.ec2.Vpc(`${name}-vpc`, {
      cidrBlock: args.cidrBlock,
      enableDnsHostnames: true,
    }, { parent: this });

    this.vpcId = vpc.id;
    this.registerOutputs({ vpcId: this.vpcId });
  }
}

The advantage: standard software engineering practices (inheritance, composition, interfaces, unit tests) apply directly. The disadvantage: this requires software engineering discipline that not all infrastructure teams possess.

CI/CD Integration

Terraform CI/CD Pipeline

# GitHub Actions example
name: Terraform Apply
on:
  push:
    branches: [main]

jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.9.0

      - name: Terraform Init
        run: terraform init

      - name: Terraform Plan
        run: terraform plan -out=tfplan

      - name: Terraform Apply
        if: github.ref == 'refs/heads/main'
        run: terraform apply -auto-approve tfplan

Plan output can be posted as PR comments for review before merge. Tools like Atlantis, Spacelift, and Terraform Cloud provide purpose-built CI/CD for Terraform with plan approval workflows, policy checks (Sentinel/OPA), and cost estimation.

CloudFormation CI/CD

CloudFormation integrates natively with AWS CodePipeline, and Change Sets provide a built-in preview mechanism:

# Create change set for review
aws cloudformation create-change-set \
  --stack-name production-vpc \
  --template-body file://vpc.yaml \
  --change-set-name deploy-20260402

# Review changes
aws cloudformation describe-change-set \
  --stack-name production-vpc \
  --change-set-name deploy-20260402

# Execute after approval
aws cloudformation execute-change-set \
  --stack-name production-vpc \
  --change-set-name deploy-20260402

Pulumi CI/CD

Pulumi's Automation API enables embedding infrastructure operations within application code, which opens unique CI/CD patterns:

import { LocalWorkspace } from "@pulumi/pulumi/automation";

const stack = await LocalWorkspace.createOrSelectStack({
  stackName: "prod",
  projectName: "infrastructure",
  program: async () => { /* inline program */ },
});

const upResult = await stack.up({ onOutput: console.log });
console.log(`Resources: ${upResult.summary.resourceChanges}`);

Testing Infrastructure Code

Terraform Testing

Terraform 1.6+ introduced native test files (.tftest.hcl):

run "vpc_creation" {
  command = apply

  assert {
    condition     = aws_vpc.main.cidr_block == "10.0.0.0/16"
    error_message = "VPC CIDR block mismatch"
  }

  assert {
    condition     = aws_vpc.main.enable_dns_hostnames == true
    error_message = "DNS hostnames should be enabled"
  }
}

Additionally, terraform validate checks syntax and type correctness, terraform plan with -detailed-exitcode enables plan-based testing in CI, and external tools like Terratest (Go) enable full integration testing.

Pulumi Testing

Pulumi supports unit tests, property tests, and integration tests using standard language test frameworks:

import * as pulumi from "@pulumi/pulumi/runtime";
import { describe, it, expect } from "vitest";

describe("VPC", () => {
  it("should have DNS hostnames enabled", async () => {
    const vpc = new aws.ec2.Vpc("test", {
      cidrBlock: "10.0.0.0/16",
      enableDnsHostnames: true,
    });

    const dnsEnabled = await new Promise(resolve =>
      vpc.enableDnsHostnames.apply(v => resolve(v))
    );
    expect(dnsEnabled).toBe(true);
  });
});

This is Pulumi's strongest differentiation: real unit tests with mocking, assertions, and standard test runner integration. No other IaC tool provides this level of testability.

The OpenTofu Factor

Since HashiCorp relicensed Terraform under the Business Source License (BSL 1.1) in August 2023, OpenTofu has emerged as a community-maintained fork under the Linux Foundation. OpenTofu 1.8.x maintains compatibility with Terraform configurations while adding features like client-side state encryption.

For organizations concerned about license implications, OpenTofu provides a drop-in replacement. For most practical purposes in 2026, Terraform and OpenTofu configurations are interchangeable. The ecosystem (providers, modules) is shared.

Decision Framework

Choose Terraform when: - You operate across multiple clouds - Your team has dedicated infrastructure engineers comfortable with HCL - You need the largest ecosystem of providers and modules - You want flexibility in CI/CD tooling (Atlantis, Spacelift, Terraform Cloud, env0)

Choose CloudFormation when: - You are 100% AWS with no plans to change - You want zero state management overhead - You need native integration with AWS Organizations, StackSets, and Service Catalog - Your team prefers YAML/JSON and AWS-native tooling

Choose Pulumi when: - Your infrastructure team consists primarily of software engineers - You value testability and type safety - You want to share code between application and infrastructure - You need the Automation API for platform engineering / self-service infrastructure

Practical Recommendation for 2026

For most teams starting fresh, Terraform remains the default choice due to ecosystem breadth, hiring pool (most infrastructure engineers know HCL), and provider coverage. If you are building an internal developer platform or your team writes application code daily, Pulumi deserves serious evaluation for its testing and abstraction capabilities.

CloudFormation is the right answer when your organization has standardized on AWS and wants to minimize toolchain complexity. Do not fight this if it is working for your team.

For structured learning on Terraform, modules, and multi-cloud IaC patterns, the DevOps with Terraform course covers everything from terraform init to production-grade module development with CI/CD integration. The DevOps Tools collection includes pre-built Terraform modules, CloudFormation templates, and Pulumi component libraries ready for production use.

For organizations building multi-cloud platforms, the Enterprise Bundles collection provides reference architectures and IaC templates covering AWS, Azure, and GCP deployment patterns that have been validated in production environments.

Start with one tool, master it, and expand based on actual multi-cloud requirements rather than speculative future needs. The best IaC tool is the one your team will consistently use.

Kehinde Ogunlowo

Senior Multi-Cloud DevSecOps Architect & AI Engineer

AWS, Azure, GCP Certified | Secret Clearance | FedRAMP, CMMC, HIPAA

Enterprise experience at Cigna Healthcare, Lockheed Martin, NantHealth, BP Refinery, and Patterson UTI.

Start Your Cloud Career Today

Access 17 free courses covering AWS, Azure, GCP, DevOps, AI/ML, and cloud security — built by a practicing Senior Cloud Architect with enterprise experience.

Get Free Cloud Career Resources

You might also like