Skip to main content
LadeStack LogoLadeStack
DevOps & CI/CD 6 min read November 3, 2024

Infrastructure as Code: Comparing Terraform, OpenTofu, and Pulumi in Modern Cloud Environments

Architectural comparison of Infrastructure as Code: declarative HCL vs general-purpose programming with Pulumi, state management, concurrency locking, drift detection, and policy-as-code.

Infrastructure as Code Architecture with Terraform and Pulumi

1. The Evolution of Infrastructure as Code (IaC)

Infrastructure as Code (IaC) fundamentally altered system administration by replacing manual cloud console clicks and ad-hoc Bash scripts with version-controlled, auditable, and repeatable software artifacts. Rather than relying on operator memory, modern infrastructure is defined as software and synthesized through automated execution engines.

Today, cloud engineering teams face an architectural choice between two distinct paradigms:

  1. Domain-Specific Declarative Languages (DSL): Championed by HashiCorp Terraform and the open-source OpenTofu fork (under the Linux Foundation), utilizing HashiCorp Configuration Language (HCL).
  2. General-Purpose Programmatic IaC: Championed by Pulumi and AWS CDK, utilizing languages such as TypeScript, Python, Go, and C#.
+--------------------------------------------------------------------------+
|                       Infrastructure as Code Models                      |
+------------------------------------+-------------------------------------+
                                     |
           +-------------------------+-------------------------+
           |                                                   |
           v                                                   v
+-------------------------------+           +-------------------------------+
|    Declarative DSL (HCL)      |           |     Programmatic (Pulumi)     |
|   Terraform / OpenTofu        |           |   TypeScript / Python / Go    |
+---------------+---------------+           +---------------+---------------+
                |                                           |
                v                                           v
+-------------------------------+           +-------------------------------+
| Static AST Parsing            |           | Language Runtime Execution    |
| Dependency Graph Resolution   |           | Resource Object Registration  |
+---------------+---------------+           +---------------+---------------+
                |                                           |
                +---------------------+---------------------+
                                      |
                                      v
                      +-------------------------------+
                      | State Storage & Concurrency   |
                      | S3 / DynamoDB / Pulumi Cloud  |
                      +---------------+---------------+
                                      |
                                      v
                      +-------------------------------+
                      | Cloud Provider APIs           |
                      | (AWS, Azure, GCP, Kubernetes) |
                      +-------------------------------+

2. Declarative HCL vs. Programmatic SDKs

The Terraform / OpenTofu Model (HCL)

HCL is intentionally constrained. It enforces a structural schema that prevents arbitrary side-effects during execution. Every engineer reading an HCL block understands its structural intent immediately:

# main.tf (Terraform / OpenTofu)
terraform {
  required_version = ">= 1.6.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.40"
    }
  }
  backend "s3" {
    bucket         = "ladestack-terraform-state-prod"
    key            = "networking/vpc.tfstate"
    region         = "us-east-1"
    dynamodb_table = "ladestack-tf-locks"
    encrypt        = true
  }
}

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

  tags = {
    Name        = "production-core-vpc"
    Environment = "production"
    ManagedBy   = "Terraform"
  }
}

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

  tags = {
    Name = "public-subnet-${count.index + 1}"
  }
}

The Pulumi Model (TypeScript / General-Purpose Languages)

Pulumi executes real language runtimes (Node.js, Python, Go runtime). This enables native software engineering practices: package managers (npm, pip), type checking, unit tests (jest, pytest), loops, classes, and reusable component abstractions:

// index.ts (Pulumi TypeScript)
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const config = new pulumi.Config();
const environment = config.require("environment");

// Reusable custom component resource
export class CoreVpc extends pulumi.ComponentResource {
  public readonly vpc: aws.ec2.Vpc;
  public readonly subnets: aws.ec2.Subnet[] = [];

  constructor(name: string, cidr: string, opts?: pulumi.ComponentResourceOptions) {
    super("ladestack:network:CoreVpc", name, {}, opts);

    this.vpc = new aws.ec2.Vpc(`${name}-vpc`, {
      cidrBlock: cidr,
      enableDnsHostnames: true,
      enableDnsSupport: true,
      tags: { Environment: environment, ManagedBy: "Pulumi" },
    }, { parent: this });

    const zones = aws.getAvailabilityZonesSync({ state: "available" }).names;

    for (let i = 0; i < 3; i++) {
      const subnet = new aws.ec2.Subnet(`${name}-subnet-${i + 1}`, {
        vpcId: this.vpc.id,
        cidrBlock: `10.0.${i * 16}.0/20`,
        availabilityZone: zones[i],
        tags: { Name: `${name}-subnet-${i + 1}` },
      }, { parent: this });

      this.subnets.push(subnet);
    }

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

const network = new CoreVpc("prod-network", "10.0.0.0/16");
export const vpcId = network.vpc.id;

3. State Management, Locking, and Secrets

Both frameworks rely on an immutable representation of real-world state to calculate diffs between desired code and deployed reality.

State Hazards and Remote Locking

  • Terraform / OpenTofu: Stores state as a JSON document. In concurrent environments, teams must configure state locking (e.g., AWS DynamoDB, HashiCorp HCP Terraform, or cloud storage object locking) to prevent two parallel pipeline runs from executing conflicting mutations, which corrupts the state file.
  • Pulumi: Connects by default to Pulumi Cloud for managed state storage and automated concurrency locking, but supports pluggable self-hosted backends (AWS S3, Azure Blob, Google Cloud Storage).

Secrets Handling in State

A critical security liability in IaC is that state files contain plaintext secrets generated or referenced during execution (database master passwords, TLS private keys, IAM access tokens).

  • Terraform: The terraform.tfstate file stores all outputs in unencrypted plaintext JSON within the storage bucket. Teams must enforce strict bucket encryption (KMS CMK), IAM least privilege, and avoid writing raw secret outputs into state whenever possible.
  • Pulumi: Built-in secret encryption encrypts secret values before they are written to state, using either Pulumi’s managed encryption key or cloud KMS providers (AWS KMS, GCP KMS, Azure Key Vault, HashiCorp Vault).

4. Policy-as-Code and Guardrails

To prevent insecure configurations from ever reaching production, teams integrate automated policy enforcement into pull requests:

Framework Policy Engine Enforcement Mechanism
Terraform (Enterprise) HashiCorp Sentinel Proprietary policy evaluation on planned state
OpenTofu / Terraform Open Policy Agent (OPA) / Rego Validates terraform show -json plan.tfplan output
Pulumi CrossGuard Native TypeScript/Python policy rules evaluated before resource provisioning

Example Pulumi CrossGuard Policy (TypeScript)

import { PolicyPack, validateResourceOfType } from "@pulumi/policy";
import * as aws from "@pulumi/aws";

new PolicyPack("security-guardrails", {
  policies: [
    {
      name: "s3-no-public-read",
      description: "Prohibits S3 buckets from having public read permissions.",
      enforcementLevel: "mandatory",
      validateResource: validateResourceOfType(aws.s3.Bucket, (bucket, args, reportViolation) => {
        if (bucket.acl === "public-read" || bucket.acl === "public-read-write") {
          reportViolation("S3 buckets must not have public ACLs.");
        }
      }),
    },
  ],
});

5. Architectural Decision Matrix: Which Should You Choose?

Evaluation Criteria Terraform / OpenTofu Pulumi
Team Skillset Operations / DevOps engineers proficient in YAML/HCL Full-stack & platform software engineering teams
Logic & Abstraction Limited (HCL loops, dynamic blocks, modules) Unlimited (OOP, inheritance, libraries, test harnesses)
Ecosystem Maturity Vast community provider ecosystem Uses Terraform bridge providers + Native Cloud SDKs
Testing Integration tests via terratest (Go) Native unit testing (jest, mocha, pytest) without provisioning
Ecosystem Risk Terraform uses BSL 1.1; OpenTofu is MPL 2.0 open-source Open-source core engine with commercial SaaS backends

IaC Production Readiness Checklist

  • Remote backend configured with server-side encryption and strict access logging.
  • Distributed concurrency state locking enabled (DynamoDB or equivalent).
  • State secrets encrypted at rest using dedicated KMS customer-managed keys.
  • CI/CD pipelines run automated plan / preview checks on pull requests.
  • Policy-as-code scanners (OPA / CrossGuard) block insecure infrastructure commits.
  • Drift detection scheduled periodically to detect out-of-band console modifications.
  • Resource deletion protection (prevent_destroy / protect) enabled on critical databases.

More Engineering Articles