Introduction
Infrastructure as Code (IaC) has revolutionized how organizations manage cloud infrastructure. Instead of manually clicking through cloud consoles, IaC enables teams to define infrastructure in code, version it, test it, and deploy it consistently. However, choosing the right IaC tool is criticalโthe wrong choice can lead to vendor lock-in, steep learning curves, and maintenance nightmares.
This comprehensive guide compares three major IaC platformsโTerraform, CloudFormation, and Pulumiโwith practical examples and real-world deployment patterns.
Core Concepts & Terminology
Infrastructure as Code (IaC)
Managing infrastructure through code rather than manual processes, enabling version control, testing, and automation.
Declarative IaC
Specifying desired state; the tool determines how to achieve it (Terraform, CloudFormation).
Imperative IaC
Specifying exact steps to achieve desired state (Pulumi, scripts).
State Management
Tracking current infrastructure state to determine what changes are needed.
Plan
Preview of changes that will be applied to infrastructure.
Apply
Executing planned changes to create or modify infrastructure.
Destroy
Removing infrastructure managed by IaC.
Module
Reusable collection of resources and configurations.
Variable
Input parameter for IaC configurations.
Output
Value exported from IaC configuration for use by other systems.
Provider
Plugin enabling IaC tool to interact with specific cloud platform (AWS, Azure, GCP).
Resource
Individual infrastructure component (EC2 instance, S3 bucket, RDS database).
Data Source
Read-only reference to existing infrastructure.
IaC Platform Comparison
Feature Comparison Matrix
| Feature | Terraform | CloudFormation | Pulumi |
|---|---|---|---|
| Language | HCL | JSON/YAML | Python, Go, TypeScript, C# |
| Cloud Support | Multi-cloud | AWS only | Multi-cloud |
| Learning Curve | Moderate | Steep | Gentle (if familiar with language) |
| State Management | Explicit | Implicit | Explicit |
| Modularity | Excellent | Good | Excellent |
| Community | Largest | Medium | Growing |
| Cost | Free | Free | Free (open-source) |
| Enterprise Support | Terraform Cloud | AWS Support | Pulumi Cloud |
| Best For | Multi-cloud | AWS-only | Polyglot teams |
Terraform
Architecture Overview
Terraform Configuration
โโโ main.tf
โโโ variables.tf
โโโ outputs.tf
โโโ terraform.tfvars
โโโ terraform.tfstate
Workflow:
1. Write configuration (HCL)
2. terraform init (initialize)
3. terraform plan (preview changes)
4. terraform apply (execute changes)
5. terraform destroy (remove resources)
Basic Configuration Example
# Configure AWS provider
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
# Variables
variable "aws_region" {
description = "AWS region"
type = string
default = "us-east-1"
}
variable "environment" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "instance_count" {
description = "Number of instances"
type = number
default = 2
}
# VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
}
}
# Subnets
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 1}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.environment}-public-subnet-${count.index + 1}"
}
}
# Security Group
resource "aws_security_group" "web" {
name = "${var.environment}-web-sg"
description = "Security group for web servers"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.environment}-web-sg"
}
}
# EC2 Instances
resource "aws_instance" "web" {
count = var.instance_count
ami = data.aws_ami.ubuntu.id
instance_type = "t3.medium"
subnet_id = aws_subnet.public[count.index % 2].id
vpc_security_group_ids = [aws_security_group.web.id]
user_data = base64encode(templatefile("${path.module}/user_data.sh", {
environment = var.environment
}))
tags = {
Name = "${var.environment}-web-${count.index + 1}"
}
depends_on = [aws_internet_gateway.main]
}
# Load Balancer
resource "aws_lb" "main" {
name = "${var.environment}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.web.id]
subnets = aws_subnet.public[*].id
tags = {
Name = "${var.environment}-alb"
}
}
resource "aws_lb_target_group" "web" {
name = "${var.environment}-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.main.id
target_type = "instance"
health_check {
healthy_threshold = 2
unhealthy_threshold = 2
timeout = 3
interval = 30
path = "/"
matcher = "200"
}
}
resource "aws_lb_target_group_attachment" "web" {
count = var.instance_count
target_group_arn = aws_lb_target_group.web.arn
target_id = aws_instance.web[count.index].id
port = 80
}
resource "aws_lb_listener" "web" {
load_balancer_arn = aws_lb.main.arn
port = "80"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.web.arn
}
}
# Data sources
data "aws_availability_zones" "available" {
state = "available"
}
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# Outputs
output "load_balancer_dns" {
description = "DNS name of load balancer"
value = aws_lb.main.dns_name
}
output "instance_ips" {
description = "Private IPs of instances"
value = aws_instance.web[*].private_ip
}
Terraform Modules
# modules/vpc/main.tf
variable "cidr_block" {
type = string
}
variable "environment" {
type = string
}
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
enable_dns_hostnames = true
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
}
}
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 1}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "${var.environment}-public-subnet-${count.index + 1}"
}
}
output "vpc_id" {
value = aws_vpc.main.id
}
output "subnet_ids" {
value = aws_subnet.public[*].id
}
# main.tf (using module)
module "vpc" {
source = "./modules/vpc"
cidr_block = "10.0.0.0/16"
environment = var.environment
}
module "security_group" {
source = "./modules/security_group"
vpc_id = module.vpc.vpc_id
environment = var.environment
}
Terraform State Management
# Backend configuration
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
# Remote state reference
data "terraform_remote_state" "vpc" {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "prod/vpc/terraform.tfstate"
region = "us-east-1"
}
}
resource "aws_security_group" "app" {
vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id
# ...
}
CloudFormation
Architecture Overview
CloudFormation Template
โโโ AWSTemplateFormatVersion
โโโ Description
โโโ Parameters
โโโ Resources
โโโ Outputs
โโโ Metadata
Workflow:
1. Create template (JSON/YAML)
2. Create stack
3. Monitor stack events
4. Update stack
5. Delete stack
Basic Template Example
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Web application infrastructure'
Parameters:
EnvironmentName:
Type: String
Default: prod
AllowedValues: [dev, staging, prod]
InstanceCount:
Type: Number
Default: 2
MinValue: 1
MaxValue: 10
Resources:
# VPC
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-vpc'
# Public Subnets
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.1.0/24
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-public-subnet-1'
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.2.0/24
AvailabilityZone: !Select [1, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-public-subnet-2'
# Internet Gateway
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-igw'
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
# Route Table
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-public-rt'
PublicRoute:
Type: AWS::EC2::Route
DependsOn: AttachGateway
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
SubnetRouteTableAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable
SubnetRouteTableAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicRouteTable
# Security Group
WebSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for web servers
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-web-sg'
# EC2 Instances
WebInstance1:
Type: AWS::EC2::Instance
Properties:
ImageId: !Sub '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2}}'
InstanceType: t3.medium
SubnetId: !Ref PublicSubnet1
SecurityGroupIds:
- !Ref WebSecurityGroup
UserData:
Fn::Base64: |
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-web-1'
WebInstance2:
Type: AWS::EC2::Instance
Properties:
ImageId: !Sub '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2}}'
InstanceType: t3.medium
SubnetId: !Ref PublicSubnet2
SecurityGroupIds:
- !Ref WebSecurityGroup
UserData:
Fn::Base64: |
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-web-2'
# Load Balancer
LoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: !Sub '${EnvironmentName}-alb'
Type: application
Scheme: internet-facing
SecurityGroups:
- !Ref WebSecurityGroup
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-alb'
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !Sub '${EnvironmentName}-tg'
Port: 80
Protocol: HTTP
VpcId: !Ref VPC
TargetType: instance
Targets:
- Id: !Ref WebInstance1
- Id: !Ref WebInstance2
HealthCheckEnabled: true
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckIntervalSeconds: 30
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
UnhealthyThresholdCount: 2
Listener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref LoadBalancer
Port: 80
Protocol: HTTP
DefaultActions:
- Type: forward
TargetGroupArn: !Ref TargetGroup
Outputs:
LoadBalancerDNS:
Description: DNS name of load balancer
Value: !GetAtt LoadBalancer.DNSName
Export:
Name: !Sub '${EnvironmentName}-alb-dns'
VPCId:
Description: VPC ID
Value: !Ref VPC
Export:
Name: !Sub '${EnvironmentName}-vpc-id'
Pulumi
Architecture Overview
Pulumi Program
โโโ __main__.py (or main.go, main.ts)
โโโ Pulumi.yaml
โโโ Pulumi.prod.yaml
Workflow:
1. Write program (Python/Go/TypeScript/C#)
2. pulumi up (preview and apply)
3. pulumi destroy (remove resources)
Python Example
import pulumi
import pulumi_aws as aws
# Configuration
config = pulumi.Config()
environment = config.get('environment') or 'prod'
instance_count = config.get_int('instance_count') or 2
# VPC
vpc = aws.ec2.Vpc('main',
cidr_block='10.0.0.0/16',
enable_dns_hostnames=True,
enable_dns_support=True,
tags={
'Name': f'{environment}-vpc',
'Environment': environment,
})
# Subnets
subnets = []
for i in range(2):
subnet = aws.ec2.Subnet(f'public-{i}',
vpc_id=vpc.id,
cidr_block=f'10.0.{i+1}.0/24',
availability_zone=aws.get_availability_zones(state='available').names[i],
map_public_ip_on_launch=True,
tags={
'Name': f'{environment}-public-subnet-{i+1}',
})
subnets.append(subnet)
# Internet Gateway
igw = aws.ec2.InternetGateway('main',
vpc_id=vpc.id,
tags={
'Name': f'{environment}-igw',
})
# Route Table
route_table = aws.ec2.RouteTable('public',
vpc_id=vpc.id,
routes=[
aws.ec2.RouteTableRouteArgs(
cidr_block='0.0.0.0/0',
gateway_id=igw.id,
),
],
tags={
'Name': f'{environment}-public-rt',
})
# Associate subnets with route table
for i, subnet in enumerate(subnets):
aws.ec2.RouteTableAssociation(f'public-{i}',
subnet_id=subnet.id,
route_table_id=route_table.id)
# Security Group
security_group = aws.ec2.SecurityGroup('web',
vpc_id=vpc.id,
description='Security group for web servers',
ingress=[
aws.ec2.SecurityGroupIngressArgs(
protocol='tcp',
from_port=80,
to_port=80,
cidr_blocks=['0.0.0.0/0'],
),
aws.ec2.SecurityGroupIngressArgs(
protocol='tcp',
from_port=443,
to_port=443,
cidr_blocks=['0.0.0.0/0'],
),
],
egress=[
aws.ec2.SecurityGroupEgressArgs(
protocol='-1',
from_port=0,
to_port=0,
cidr_blocks=['0.0.0.0/0'],
),
],
tags={
'Name': f'{environment}-web-sg',
})
# Get latest Ubuntu AMI
ubuntu_ami = aws.ec2.get_ami(
most_recent=True,
owners=['099720109477'],
filters=[
aws.ec2.GetAmiFilterArgs(
name='name',
values=['ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*'],
),
])
# EC2 Instances
instances = []
for i in range(instance_count):
instance = aws.ec2.Instance(f'web-{i}',
ami=ubuntu_ami.id,
instance_type='t3.medium',
subnet_id=subnets[i % 2].id,
vpc_security_group_ids=[security_group.id],
user_data='''#!/bin/bash
apt-get update
apt-get install -y apache2
systemctl start apache2
systemctl enable apache2
''',
tags={
'Name': f'{environment}-web-{i+1}',
})
instances.append(instance)
# Load Balancer
load_balancer = aws.lb.LoadBalancer('main',
internal=False,
load_balancer_type='application',
security_groups=[security_group.id],
subnets=[subnet.id for subnet in subnets],
tags={
'Name': f'{environment}-alb',
})
# Target Group
target_group = aws.lb.TargetGroup('web',
port=80,
protocol='HTTP',
vpc_id=vpc.id,
target_type='instance',
health_check=aws.lb.TargetGroupHealthCheckArgs(
healthy_threshold=2,
unhealthy_threshold=2,
timeout=3,
interval=30,
path='/',
matcher='200',
),
tags={
'Name': f'{environment}-tg',
})
# Target Group Attachments
for i, instance in enumerate(instances):
aws.lb.TargetGroupAttachment(f'web-{i}',
target_group_arn=target_group.arn,
target_id=instance.id,
port=80)
# Listener
listener = aws.lb.Listener('web',
load_balancer_arn=load_balancer.arn,
port=80,
protocol='HTTP',
default_actions=[
aws.lb.ListenerDefaultActionArgs(
type='forward',
target_group_arn=target_group.arn,
),
])
# Outputs
pulumi.export('load_balancer_dns', load_balancer.dns_name)
pulumi.export('instance_ips', [instance.private_ip for instance in instances])
Best Practices & Common Pitfalls
Best Practices
- Version Control: Store IaC in Git with proper branching
- State Management: Use remote state with encryption and locking
- Modularity: Create reusable modules for common patterns
- Testing: Test IaC changes in non-production environments
- Documentation: Document infrastructure decisions and configurations
- Secrets Management: Never commit secrets, use secure storage
- Code Review: Review IaC changes before applying
- Monitoring: Monitor infrastructure changes and drift
- Disaster Recovery: Test recovery procedures regularly
- Cost Optimization: Review infrastructure costs regularly
Common Pitfalls
- State Corruption: Losing or corrupting state files
- Manual Changes: Making manual changes outside IaC
- Tight Coupling: Infrastructure too tightly coupled
- Inadequate Testing: Deploying untested infrastructure
- Secrets in Code: Committing secrets to version control
- No Rollback Plan: Unable to revert failed deployments
- Insufficient Documentation: Team can’t understand infrastructure
- Drift Detection: Not detecting infrastructure drift
- Scaling Issues: Infrastructure not scaling properly
- Cost Overruns: Unexpected infrastructure costs
External Resources
Terraform
CloudFormation
Pulumi
Conclusion
Infrastructure as Code is essential for modern cloud operations. Terraform excels for multi-cloud deployments with its large ecosystem and community. CloudFormation is ideal for AWS-only environments with tight AWS integration. Pulumi offers flexibility for teams comfortable with programming languages.
Regardless of tool choice, focus on proper state management, modularity, testing, and documentation. Invest in automation and monitoring to ensure infrastructure reliability and cost efficiency.
Start with simple infrastructure, gradually add complexity, and continuously optimize based on real-world experience and feedback.
Comments