Skip to main content
โšก Calmops

Infrastructure as Code Deep Dive: Terraform, CloudFormation, and Pulumi

Introduction

Infrastructure as Code (IaC) has transformed how teams provision and manage cloud infrastructure. Instead of manually clicking through cloud consoles, IaC enables declarative definitions of infrastructure that are version-controlled, tested, and automated. This shift brings dramatic improvements in consistency, velocity, and reliability.

Understanding IaC tools and practices is essential for modern cloud engineering. Whether managing a few resources or complex multi-environment architectures, IaC provides the foundation for reproducible, manageable infrastructure.

This comprehensive guide examines IaC across major tools. We explore Terraform, CloudFormation, and Pulumiโ€”understanding their strengths, tradeoffs, and appropriate use cases. Whether establishing IaC practices or improving existing implementations, this guide provides the knowledge necessary for success.

Understanding Infrastructure as Code

IaC defines infrastructure through code rather than manual processes.

Benefits of IaC

  • Consistency: Same infrastructure every time
  • Version Control: Track changes, enable code review
  • Automation: Reduce manual effort, improve velocity
  • Testing: Validate infrastructure before deployment
  • Documentation: Code serves as documentation

IaC Approaches

Approach Description Tools
Declarative Define desired state Terraform, Pulumi, CloudFormation
Imperative Define steps to achieve state AWS CLI, scripts

Terraform

Terraform uses a declarative approach with a domain-specific language (HCL).

Basic Configuration

# main.tf
terraform {
  required_version = ">= 1.0"
  
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  
  backend "s3" {
    bucket = "terraform-state-bucket"
    key    = "prod/terraform.tfstate"
    region = "us-east-1"
  }
}

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

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

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block             = "10.0.1.0/24"
  availability_zone       = "us-east-1a"
  map_public_ip_on_launch = true
  
  tags = {
    Name = "public-subnet"
  }
}

Variables and Outputs

# variables.tf
variable "environment" {
  description = "Environment name"
  type        = string
  default     = "production"
}

variable "vpc_cidr" {
  description = "VPC CIDR block"
  type        = string
  default     = "10.0.0.0/16"
}

variable "availability_zones" {
  description = "Availability zones"
  type        = list(string)
  default     = ["us-east-1a", "us-east-1b"]
}

# outputs.tf
output "vpc_id" {
  description = "VPC ID"
  value       = aws_vpc.main.id
}

output "subnet_ids" {
  description = "Subnet IDs"
  value       = aws_subnet.public[*].id
}

Modules

# modules/vpc/main.tf
module "vpc" {
  source = "./modules/vpc"
  
  environment = var.environment
  cidr_block  = "10.0.0.0/16"
  
  availability_zones = ["us-east-1a", "us-east-1b"]
  
  tags = {
    ManagedBy = "Terraform"
  }
}

# modules/vpc/variables.tf
variable "environment" {
  type = string
}

variable "cidr_block" {
  type = string
}

variable "availability_zones" {
  type = list(string)
}

variable "tags" {
  type    = map(string)
  default = {}
}

# modules/vpc/main.tf
resource "aws_vpc" "main" {
  cidr_block = var.cidr_block
  
  tags = merge(var.tags, {
    Environment = var.environment
  })
}

State Management

# Terraform workflow
terraform init          # Initialize working directory
terraform plan         # Generate execution plan
terraform apply        # Apply changes
terraform destroy      # Destroy resources
terraform show        # Show current state
terraform state list   # List resources

Workspaces

# Create workspace
terraform workspace new production

# Switch workspace
terraform workspace select production

# Use workspace in config
resource "aws_instance" "app" {
  instance_type = terraform.workspace == "production" ? "t3.large" : "t3.micro"
  
  tags = {
    Environment = terraform.workspace
  }
}

AWS CloudFormation

CloudFormation provides AWS-native infrastructure as code.

Template Structure

AWSTemplateFormatVersion: '2010-09-09'
Description: 'VPC Network Template'

Parameters:
  Environment:
    Type: String
    Default: production
    AllowedValues:
      - production
      - staging
      - development
  
  VPCCidr:
    Type: String
    Default: 10.0.0.0/16

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCidr
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub ${Environment}-vpc

  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: !Select [0, !Ref AWS::NoValue]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${Environment}-public-subnet

Outputs:
  VPCId:
    Description: VPC ID
    Value: !Ref VPC
    Export:
      Name: !Sub ${AWS::StackName}-VPC-ID

Nested Stacks

# root-stack.yaml
Resources:
  NetworkStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://s3.amazonaws.com/templates-bucket/network.yaml
      Parameters:
        Environment: !Ref Environment
        VPCCidr: !Ref VPCCidr
  
  DatabaseStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://s3.amazonaws.com/templates-bucket/database.yaml
      Parameters:
        VPCId: !GetAtt NetworkStack.Outputs.VPCId
        SubnetIds: !GetAtt NetworkStack.Outputs.SubnetIds

Change Sets

# Create change set
aws cloudformation create-change-set \
  --stack-name my-stack \
  --template-body file://template.yaml \
  --change-set-type UPDATE \
  --change-set-name my-changes

# Execute change set
aws cloudformation execute-change-set \
  --stack-name my-stack \
  --change-set-name $(aws cloudformation list-change-sets --stack-name my-stack --query 'Summaries[0].ChangeSetName' --output text)

Pulumi

Pulumi uses general-purpose programming languages for infrastructure.

Program Structure

import pulumi
import pulumi_aws as aws

# Create VPC
vpc = aws.ec2.Vpc(
    "main",
    cidr_block="10.0.0.0/16",
    enable_dns_hostnames=True,
    enable_dns_support=True,
    tags={
        "Name": "main-vpc",
        "Environment": "production"
    }
)

# Create subnet
subnet = aws.ec2.Subnet(
    "public",
    vpc_id=vpc.id,
    cidr_block="10.0.1.0/24",
    availability_zone="us-east-1a",
    map_public_ip_on_launch=True,
    tags={
        "Name": "public-subnet"
    }
)

# Export values
pulumi.export("vpc_id", vpc.id)
pulumi.export("subnet_id", subnet.id)

Configuration and Secrets

import pulumi
from pulumi import Config

config = Config()

# String config
environment = config.require("environment")

# Secret config
api_key = config.require_secret("apiKey")

# Secret with default
db_password = config.get_secret("dbPassword") or "default-password"

# Require object
database_config = require_object("database")

Component Resources

class NetworkComponent(pulumi.ComponentResource):
    def __init__(self, name, opts=None):
        super().__init__("custom:network:Network", name, opts=opts)
        
        # VPC
        self.vpc = aws.ec2.Vpc(
            f"{name}-vpc",
            cidr_block="10.0.0.0/16",
            opts=pulumi.ResourceOptions(parent=self)
        )
        
        # Subnet
        self.subnet = aws.ec2.Subnet(
            f"{name}-subnet",
            vpc_id=self.vpc.id,
            cidr_block="10.0.1.0/24",
            opts=pulumi.ResourceOptions(parent=self)
        )
        
        self.register_outputs({
            "vpc_id": self.vpc.id,
            "subnet_id": self.subnet.id
        })

# Usage
network = NetworkComponent("main")

Best Practices

State Management

# Terraform with remote state
terraform {
  backend "s3" {
    bucket         = "terraform-state-prod"
    key            = "network/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

Testing Infrastructure

# Terratest example
import pytest
import terraform
import aws

def test_vpc_created():
    # Run terraform
    options = terraform.Options(
        terraform_dir=".",
        vars={
            "environment": "test"
        }
    )
    
    # Apply and get outputs
    output = terraform.apply(options, return_output=True)
    
    # Verify VPC exists
    vpc_id = output["vpc_id"]
    vpc = aws.ec2.Vpc(vpc_id)
    assert vpc.cidr_block == "10.0.0.0/16"
    
    # Cleanup
    terraform.destroy(options)

CI/CD Integration

# GitHub Actions workflow
name: Terraform CI

on:
  pull_request:
    paths:
      - '**.tf'
  push:
    paths:
      - '**.tf'
    branches:
      - main

jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: 1.0
      
      - name: Terraform Init
        run: terraform init
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      
      - name: Terraform Plan
        run: terraform plan -no-color
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      
      - name: Terraform Apply
        if: github.ref == 'refs/heads/main'
        run: terraform apply -auto-approve
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Tool Comparison

Feature Terraform CloudFormation Pulumi
Language HCL YAML/JSON Python, TS, Go
State File/Remote Managed File/Remote
Providers Many AWS only Many
Testing Terratest CFN Nitro Native testing
Learning Curve Medium Low Medium

Conclusion

Infrastructure as Code is essential for modern cloud engineering. Understanding Terraform, CloudFormation, and Pulumi enables informed decisions about which tools to use and how to implement IaC practices effectively.

Key practices include starting with remote state and state locking, organizing code into modules for reuse, implementing testing as part of the workflow, and integrating with CI/CD for automated deployments. The investment in IaC pays dividends through improved consistency, velocity, and reliability.

As infrastructure grows in complexity, IaC becomes increasingly critical. Choose the tool that fits your team’s skills and requirements, then invest in building robust IaC practices.


Resources

Comments