Skip to content
SP StackPractices
intermediate

Infrastructure as Code — Terraform and Pulumi

A practical guide to managing infrastructure as code: benefits of declarative vs imperative approaches, state management, modules, and testing infrastructure changes.

Topics: devops

Infrastructure as Code — Terraform and Pulumi

Introduction

Infrastructure as Code (IaC) is the practice of managing and provisioning infrastructure through machine-readable definition files rather than manual configuration. It turns infrastructure changes into repeatable, reviewable, and versioned operations. This guide compares declarative and imperative approaches, covers Terraform and Pulumi, and provides best practices for production use.

Declarative vs Imperative

ApproachYou SayTool HandlesExamples
Declarative”I want this state”How to get thereTerraform, CloudFormation, Pulumi
Imperative”Do these steps”Execution orderAnsible, Shell scripts, SDK calls

Declarative is preferred for infrastructure because it handles drift detection, dependency ordering, and idempotency automatically.

Terraform

HashiCorp’s declarative tool. Define resources in HCL (HashiCorp Configuration Language) and Terraform plans and applies the changes.

# main.tf — define a VPC and subnet
terraform {
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}

provider "aws" {
  region = "us-east-1"
}

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  tags = { Name = "production-vpc" }
}

resource "aws_subnet" "public" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"
  tags = { Name = "public-subnet" }
}

Terraform Workflow

terraform init      # Download providers and modules
terraform plan      # Preview changes
terraform apply     # Execute changes
terraform destroy   # Tear down (use with caution)

State Management

Terraform stores the mapping between your configuration and real resources in a state file.

State StorageBest For
Local fileSolo development only
S3 + DynamoDBTeam workflows with locking
Terraform CloudCollaboration, remote execution, policy checks
# Remote state backend
terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "prod/vpc.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

Critical rule: Never edit state files manually. Use terraform state commands or fix the configuration and re-apply.

Modules

Modules are reusable, parameterized infrastructure components.

# modules/vpc/main.tf
variable "cidr_block" { type = string }
variable "azs" { type = list(string) }

resource "aws_vpc" "this" {
  cidr_block = var.cidr_block
  tags = { Name = "vpc" }
}

# root module usage
module "vpc" {
  source     = "./modules/vpc"
  cidr_block = "10.0.0.0/16"
  azs        = ["us-east-1a", "us-east-1b"]
}

Best practice: Publish internal modules to a private registry. Version them like software.

Pulumi

Pulumi uses general-purpose programming languages (TypeScript, Python, Go, C#) instead of HCL.

// TypeScript: define a VPC with Pulumi
import * as aws from "@pulumi/aws";

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

const subnet = new aws.ec2.Subnet("public", {
    vpcId: vpc.id,
    cidrBlock: "10.0.1.0/24",
    tags: { Name: "public-subnet" },
});

export const vpcId = vpc.id;

When to Choose Pulumi Over Terraform

Use Pulumi WhenUse Terraform When
Your team prefers programming languages over HCLYour team already knows HCL
You need complex logic (loops, conditionals, abstraction)Your infrastructure is mostly static resource declarations
You want to test infrastructure with unit testsYou want the largest ecosystem of modules and community support
You are building a platform or internal developer toolingYou need maturity and broad provider support

Testing Infrastructure

Static Analysis

Validate before applying.

# Terraform: format and validate
terraform fmt -check
terraform validate

# TFLint: catch provider-specific mistakes
tflint --deep

# Checkov / Terraform-compliance: security policies
checkov -d .

Plan Review

Always review the plan in CI before applying.

terraform plan -out=tfplan
terraform show -json tfplan > plan.json
# CI parses plan.json and checks for destructive changes

Integration Testing

Test that the infrastructure actually works.

ToolApproach
Terratest (Go)Apply infrastructure, run assertions, destroy
Kitchen-TerraformRuby-based testing with Inspec
Pulumi unit testsMock providers and assert resource properties
// Terratest example
func TestVpc(t *testing.T) {
    terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
        TerraformDir: "../examples/vpc",
    })
    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    vpcId := terraform.Output(t, terraformOptions, "vpc_id")
    assert.NotEmpty(t, vpcId)
}

Best Practices

  • Store code in version control — every infrastructure change is a PR, reviewed, and logged
  • Use remote state with locking — prevents concurrent modifications corrupting state
  • Separate environments — use workspaces or separate state files per environment
  • Use modules for reusability — but avoid over-abstraction; simple is better
  • Never commit secrets — use secret managers (AWS Secrets Manager, Vault) and reference by ARN
  • Document your modules — README with inputs, outputs, and usage examples
  • Pin provider versions — prevent breaking changes from automatic provider updates

Common Mistakes

  • Running terraform apply locally instead of through CI/CD
  • Storing state files in Git (contains sensitive resource IDs and sometimes secrets)
  • Not using workspaces or separate directories for environments
  • Writing giant monolithic Terraform configs instead of modular components
  • Ignoring plan output — the plan tells you what will be destroyed; read it
  • Using count or for_each with resources that cannot be recreated without downtime

Frequently Asked Questions

Can I use Terraform and Pulumi together?

Yes, via Terraform state references or Pulumi’s Terraform bridge. Migrate gradually: import existing Terraform state into Pulumi, or manage new resources with Pulumi while Terraform handles legacy.

How do I handle secrets in IaC?

Never hardcode secrets. Use:

  • Environment variables for non-sensitive config
  • Secret managers (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault) for sensitive values
  • Terraform sensitive = true on outputs to prevent logging

Should I apply Terraform from my laptop or CI/CD?

Always from CI/CD. Local applies are untraceable, unreviewed, and bypass approval workflows. Use Terraform Cloud, Atlantis, or a GitOps pipeline for all production changes.