17 free courses, no signup wall
Architect-led enterprise cloud, security & AI
Fixed-price engagements, scoped on a discovery call
Skip to content

Terraform vs CloudFormation vs Pulumi: IaC for 2026

By Kehinde Ogunlowo · · Updated

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.

Because most estates now run several of these tools at once, this article starts with the orchestration layer and then compares the tools themselves.

The Orchestration Layer: Terraform, OpenTofu, Ansible, and Spacelift

Disclosure: this section was produced in partnership with Spacelift, a Citadel Cloud Management partner. The technical analysis reflects our own editorial view.

Enterprise infrastructure rarely fits cleanly into a single Infrastructure as Code tool.

Terraform or OpenTofu may be responsible for provisioning cloud infrastructure such as networks, IAM resources, compute instances, databases, and Kubernetes infrastructure. Ansible addresses a different layer of the lifecycle: configuring operating systems, installing packages, managing services, and applying application-level configuration. Using both is complementary, not redundant.

The operational challenge is what happens when these tools are managed as completely separate workflows. A typical enterprise runs one pipeline for Terraform or OpenTofu and another for Ansible. Each has its own credentials, approval process, access controls, logging, security checks, and audit trail. Platform and security teams then have to understand two operational paths for what is ultimately a single infrastructure change.

Terraform Pipeline       Ansible Pipeline
       |                       |
   Approval                Approval
       |                       |
   Credentials             Credentials
       |                       |
     Audit                   Audit

A more cohesive model treats provisioning and configuration as connected stages of the same workflow:

Terraform/OpenTofu (provision) → Ansible (configure) → validation

For example, Terraform or OpenTofu provisions a compute instance and its supporting network resources. Once those resources exist, an Ansible workflow configures the operating system, installs packages, deploys services, and applies configuration. The workflow then mirrors the real dependency between provisioning and configuration management.

Example: Provision an Application Server and Configure It

Terraform/OpenTofu creates the VPC, subnet, security group, IAM role, compute instance, DNS record, and supporting resources, then exposes outputs:

instance_id
private_ip
public_ip
environment

Ansible consumes that infrastructure information and:

  • establishes connectivity
  • installs OS packages
  • configures users
  • installs the application runtime
  • configures systemd services
  • deploys application configuration
  • applies security hardening

The orchestration layer coordinates the handoff:

Git commit / pull request
        |
        v
Terraform/OpenTofu stack  -->  provisions network, compute, IAM, databases
        |
        |  infrastructure outputs
        v
Ansible stack             -->  configures OS, packages, services, application
        |
        v
Policy checks + approvals + RBAC
        |
        v
Deployment + audit trail

The value here is dependency orchestration, not merely "running Ansible from a CI/CD system."

Where Spacelift Fits

Spacelift can act as the orchestration layer for this type of workflow. Its stack model supports Terraform, OpenTofu, Ansible, and other infrastructure technologies, so teams can coordinate operations while applying common workflow and governance controls. It sits above the individual tools rather than requiring an organization to replace them.

Terraform, OpenTofu, and Ansible continue to do the work they are designed for. A platform team can define dependencies between a Terraform or OpenTofu stack and an Ansible stack so infrastructure is provisioned before configuration is applied. Policy checks, approvals, access controls, and execution history then become part of one operational process instead of being rebuilt for every tool.

This approach is most useful when a team already runs Terraform or OpenTofu at scale and has substantial Ansible automation. Rather than forcing consolidation on one technology, the platform provides a common operating model across the existing toolchain.

How the Handoff Works in Practice

The mechanics matter more than the diagram. Based on the Spacelift documentation:

  • Dependencies and outputs. A stack dependency lets a downstream stack reference an upstream stack's outputs, which arrive as environment variables. The dependency graph must be acyclic.
  • Ansible receives, but does not provide, outputs. The docs state that Ansible stacks have no concept of outputs, so they can sit at the receiving end of a dependency but cannot be referenced as a source.
  • Only successful tracked runs count. Downstream stacks queue tracked runs behind the upstream run. If the latest upstream run failed, its outputs are not available. Proposed runs and tasks are not considered.
  • Sensitive outputs need opt-in. Passing masked outputs across stacks requires enabling sensitive-output transfer on the stack and at the worker pool level.
  • Ansible planning uses check mode. The Ansible plan phase runs --check, and not every module supports it. Playbooks may need to handle check-mode errors.
  • Approvals and automation. Autodeploy applies a change automatically when planning succeeds and no plan policy warns. Approval policies add human or rule-based gates, and trigger policies can chain runs across stacks, so guard against loops.
  • Governance. Policies are written in Rego (OPA), access is controlled through RBAC, and an audit trail is available on the Enterprise plan.
  • Drift detection scope. Drift detection requires private workers and a qualifying plan. Confirm which vendors it covers before assuming it applies to Ansible stacks.

Terraform and OpenTofu differ per stack only by workflow tool setting, and dependencies are stack-level, so mixing both in one graph should work. Test your own topology before standardizing on it.

Beyond Terraform: The Wider Spacelift Capability Set

Spacelift's documentation describes a platform broader than "Terraform CI/CD." The capabilities below are the ones most relevant to a multi-tool estate. Plan availability varies by feature, so confirm tiers with Spacelift before committing.

One workflow across many tools. Stacks can run Terraform and OpenTofu (a per-stack workflow tool setting), Terragrunt, Pulumi (C#, Go, TypeScript, Python), AWS CloudFormation, Kubernetes (anything runnable through kubectl, including Helm and Kustomize), and Ansible. Pulumi plan policies receive the raw Pulumi plan rather than Terraform's format, and module CI/CD is not available for Pulumi projects. A common pattern from the Kubernetes docs is a Terraform stack that provisions the cluster, followed by a Kubernetes stack that deploys onto it.

Policy as code. Policies are written in Rego (OPA). The documented types are login, access, approval, plan, push, trigger, notification, initialization, and intent. Plan policies give feedback after planning, approval policies decide who can approve a run, trigger policies chain stacks, and initialization policies block suspicious runs before they start. A policy workbench lets authors iterate without waiting for real runs, and policies can auto-attach to stacks by label.

Governance and identity. Spaces and RBAC scope who can do what, with custom roles and identity provider group mapping. Login policies control who gets in. Cloud access uses integrations for AWS, Azure, and GCP, including OIDC, so runs do not need long-lived static keys. The audit trail is Enterprise-only.

Drift detection. Drift detection runs scheduled proposed runs against stable stacks and can optionally trigger a reconciliation run. It is documented for Terraform, Pulumi, CloudFormation, and Kubernetes, requires private workers, and is available on Starter and above. Plan and trigger policies see a drift_detection flag so reconciliation can be governed separately.

Platform engineering and self-service. Blueprints let platform teams publish a form that creates a pre-configured stack, with typed inputs, validation, and attached policies, so consumers do not need to write IaC. Templates go further by keeping lifecycle control over the stacks they create, which suits stricter governance.

AI and agentic operations. Spacelift groups its AI features under Spacelift Intelligence:

  • Intent lets an AI client describe infrastructure in natural language. Intent discovers provider schemas from the OpenTofu registry and plans operations. Governance runs before execution through Rego policy, RBAC, and an operation history. The documentation labels it beta, and Spacelift positions it for prototyping and experimentation while IaC remains the production system of record.
  • Infra Assistant is a chat interface in the Spacelift UI. Ask mode is read-only (navigation, documentation and API search, account queries, provider schemas). Optional Build mode can create, modify, and delete cloud resources under Intent policies, with confirmation for destructive operations and actions recorded in the audit trail.
  • The Spacelift MCP server connects clients such as Claude Code, Cursor, VS Code, Codex, and Gemini. The assistant's abilities follow the user's Spacelift session permissions, so existing RBAC and login policies apply.
  • Spacelift Flows is a visual workflow builder aimed at day-2 operations, with templates for things like drift alerts, incident enrichment, and self-service provisioning through ticketing tools.

Mapping Capabilities to Operating Domains

Spacelift does not organize its documentation by AppOps, SecOps, NetOps, DataOps, or LLMOps. The mapping below is our reading of how the documented capabilities apply, not a vendor claim.

DomainHow the capabilities apply
DevOpsGit-driven stacks, proposed runs on pull requests, dependencies between Terraform/OpenTofu and Ansible
Platform engineeringBlueprints and templates, Spaces, RBAC, shared contexts
SecOpsRego policies, approval gates, initialization policies, audit trail, private workers
IdentitySSO and login policies, RBAC, OIDC cloud integrations, MCP sessions scoped to user permissions
AppOpsKubernetes stacks for Helm and Kustomize, Ansible for application configuration
NetOpsTerraform/OpenTofu or Pulumi stacks managing network resources under plan policy review
DataOps and LLMOpsProvision data and model infrastructure as stacks, with Intent or Infra Assistant for governed ad hoc changes
Day-2 operationsDrift detection, reconciliation, Flows

The consistent theme is that the orchestration layer applies one set of controls to many tools, including AI agents. An agent that provisions infrastructure through a policy-checked, audited path is easier to trust than one holding a shared credential.

Orchestrating AI and Agentic Platforms

The same orchestration layer applies to AI-native platforms, where the infrastructure is larger, changes faster, and increasingly has AI agents in the loop. The table maps common AI stack layers to what Terraform/OpenTofu provision and what an orchestration layer such as Spacelift adds. Spacelift's documentation does not organize itself around these stacks, so the mapping is our analysis built on documented capabilities.

Stack layerProvisioned as codeWhat orchestration adds
Agents and frameworks (Python, FastAPI, LangChain, LangGraph, CrewAI, AutoGen, Semantic Kernel, Google ADK)Compute, containers, queues, secrets, service identitiesKubernetes stacks (Helm, Kustomize) deploy agent services after a Terraform stack builds the cluster; dependencies enforce the order
Agent protocols (MCP, A2A)Gateways, networking, endpoints, IAMStack-level RBAC and policy on the infrastructure those endpoints run on. Spacelift itself exposes an MCP server, so a client such as Claude Code acts under the user's permissions
RAG and search (pgvector, Azure AI Search, OpenSearch, Pinecone)Databases, search domains, indexes, network rules, through provider resources where they existPlan policies review data-store changes, and drift detection flags manual edits to stores that hold embeddings
LLM platforms (Azure OpenAI, Bedrock, Vertex AI, Hugging Face endpoints)Model deployments, endpoints, quotas, GPU node pools, service accountsApproval gates on expensive or sensitive changes, and OIDC cloud integrations instead of static keys
Cloud and platform (AWS, Azure, GCP, EKS/AKS/GKE, Docker, Helm, GitOps)Accounts, networks, clusters, registriesTerraform, OpenTofu, Pulumi, CloudFormation, Kubernetes, and Ansible in one dependency graph. Bicep is not among the documented vendors, so confirm how you would run it
AI security (IAM/PAM, DevSecOps, Zero Trust, OPA, policy as code, guardrails)Roles, policies, network segmentation, secrets storesRego policies, the same OPA engine many teams already use, gate plans before apply, and initialization policies block suspicious runs
Governance (NIST AI RMF, NIST 800-53, FedRAMP, HIPAA, HITRUST, SOC 2, CMMC)Logging, encryption, and boundary controlsApproval records, run history, and an audit trail (Enterprise) serve as evidence for your own assessments

Three points make this useful rather than decorative.

Agents need their own governed path. Giving an AI agent a shared credential or a person's login is the weak pattern. With Spacelift, an agent connecting through the MCP server is limited by the session permissions of the user it acts for. Infra Assistant's Build mode is checked by Intent policies and recorded in the audit trail, and destructive operations require confirmation. Intent is documented as beta, so treat it as experimentation rather than the production path.

Policy limits the blast radius of a mistaken or compromised agent. Prompt injection and unreliable model output cannot be fully prevented at the model layer. A policy and approval layer around infrastructure changes means a bad instruction still has to pass the same plan review, approval, and RBAC as any human change. This is an architectural argument, not a guarantee.

Compliance evidence, not compliance. No tool makes an organization FedRAMP, HIPAA, or CMMC compliant. An orchestration layer produces consistent approvals, logs, and policy enforcement that feed an assessment. Whether a given Spacelift plan or hosting option meets a specific framework is a question for Spacelift and your assessor.

The application frameworks themselves, such as LangGraph, CrewAI, and Google ADK, remain your code and your runtime. The orchestration layer governs the infrastructure they run on and the changes agents are allowed to make to it.

Managing Terraform and OpenTofu Together

Coexistence is the more realistic enterprise problem than migration. Organizations do not move everything at once. They may have existing Terraform estates, new OpenTofu workloads, Ansible automation, Pulumi projects, and Kubernetes workflows side by side.

                 Orchestration layer
                         |
            +------------+------------+
            |                         |
     Terraform stack           OpenTofu stack
            |                         |
            +------------+------------+
                         |
                   Ansible stack
                         |
                  Infrastructure

The question becomes how to operate that whole estate consistently. The broader lesson: enterprise IaC strategy is increasingly about orchestrating heterogeneous tools, and standardizing the control plane, governance, security, and developer experience around the tools an organization legitimately needs, not just selecting a single provisioning language.

The Three Contenders at a Glance

DimensionTerraformCloudFormationPulumi
VendorHashiCorp (IBM)AWSPulumi Corporation
LanguageHCL (HashiCorp Configuration Language)JSON / YAMLPython, TypeScript, Go, C#, Java
Multi-cloudNative multi-cloud via providersAWS onlyNative multi-cloud via providers
State managementRemote backends (S3, Terraform Cloud, etc.)Managed by AWSPulumi Cloud or self-managed backends
PricingOpen Source (BSL 1.1) / Terraform Cloud paid tiersFree (AWS charges for resources only)Open Source / Pulumi Cloud paid tiers
Current versionTerraform 1.9.x / OpenTofu 1.8.xLatest transforms and modules (2024+)Pulumi 3.x
Learning curveMedium (new language, but declarative)Low for AWS-only teamsLow 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.

Continue Learning

Kehinde Ogunlowo

Principal AI Platform Architect & Multi-Cloud Security Architect

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

Enterprise experience across regulated cloud, security, and AI platforms in healthcare, public sector, and energy.

LinkedIn GitHub Portfolio

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