Introduction
Infrastructure as Code (IaC) means managing and provisioning infrastructure through machine-readable configuration files rather than manual processes or interactive configuration tools. It's not just "using Terraform" — it's a set of principles and practices that transform how organizations build and maintain infrastructure. This guide teaches the core principles and how to apply them with modern tools.
The Core Principles of IaC
1. Declarative vs ImperativeImperative (how to do it):
"Create a server, then install nginx, then configure port 80"
- Fragile: what if server already exists?
- Not idempotent
Declarative (what should exist):
"There should be a server running nginx on port 80"
- Tool figures out HOW to make it so
- Idempotent: running twice gives same result
- Examples: Terraform (declarative), Ansible playbooks (can be either)# This Terraform block is idempotent
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t3.medium"
tags = {
Name = "web-server"
}
}
# Run once: creates instance
# Run again: no change (already correct)
# Run with different instance_type: modifies in place or replacesYour infrastructure code should be in Git:
infrastructure/
├── terraform/
│ ├── modules/
│ └── environments/
│ ├── staging/
│ └── production/
├── ansible/
│ ├── roles/
│ └── playbooks/
└── kubernetes/
└── manifests/
Benefits:
- Every change has author, timestamp, reason (commit message)
- Roll back infrastructure like you roll back code
- Branch and test infrastructure changes before merging
- Code review for infrastructure changes (prevents mistakes)Mutable (traditional):
Deploy server → SSH in → Update nginx config → Reload nginx
Problem: Configuration drift (servers diverge over time)
Immutable:
Update config in code → Build new AMI/container → Deploy new version → Destroy old
Benefits:
- No configuration drift (every server identical)
- Easy to test (know exactly what's in the image)
- Rollback = deploy previous image versionTerraform: State Management
# terraform.tf - Configure remote state storage
terraform {
required_version = ">= 1.0"
backend "s3" {
bucket = "company-terraform-state"
key = "production/terraform.tfstate"
region = "us-east-1"
# State locking (prevent concurrent runs)
dynamodb_table = "company-terraform-locks"
encrypt = true
}
}
# State stores current infrastructure reality
# NEVER edit terraform.tfstate manually
# Use: terraform state list (see what's tracked)
# terraform state show (see details)
# terraform state mv (rename resource)
# terraform state rm (stop tracking resource)Drift Detection and Enforcement
# Terraform: detect if real infrastructure drifted from code
terraform plan
# If output shows changes even though you didn't change code:
# Someone made manual changes → DRIFT detected
# Fix drift: apply to bring back to desired state
terraform apply
# Or import the manual change into state:
terraform import aws_instance.web i-1234567890abcdef0
# Ansible: check mode (detect drift without changing)
ansible-playbook site.yml --check --diff
# Shows: would change X, Y, Z (doesn't actually change)Environment Promotion Pattern
code/
└── terraform/
└── environments/
├── dev/ ← First deployed here
│ └── main.tf (calls modules with dev values)
├── staging/ ← Same code, different variables
│ └── main.tf
└── production/ ← Same code, larger scale
└── main.tf
# All environments use same modules:
module "vpc" {
source = "../../modules/vpc"
cidr = var.vpc_cidr # dev: 10.0.0.0/16, prod: 10.1.0.0/16
}
module "eks" {
source = "../../modules/eks"
min_nodes = var.min_nodes # dev: 2, prod: 10
instance_type = var.instance_type # dev: t3.medium, prod: m5.xlarge
}Testing Infrastructure Code
# Terraform: validate syntax and logic
terraform validate
terraform fmt -check
# Security scan
tfsec ./
checkov -d ./
# Integration test with Terratest (Go)
go test -v ./test/ -timeout 30m
# Ansible: lint roles
ansible-lint roles/nginx/
yamllint roles/nginx/
# Molecule: test Ansible roles in containers
molecule testGitOps for Infrastructure
# .github/workflows/terraform.yml
name: Terraform CI/CD
on:
pull_request:
paths: ['terraform/**']
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Terraform Plan
run: |
terraform init
terraform plan -out=plan.tfplan
env:
TF_VAR_environment: staging
- name: Cost Estimate (Infracost)
uses: infracost/actions/setup@v2
with:
api-key: ${{ secrets.INFRACOST_API_KEY }}
- run: infracost diff --path=plan.tfplan
apply:
if: github.ref == 'refs/heads/main'
needs: plan
environment: staging # Requires approval
steps:
- run: terraform apply plan.tfplanDocumentation as Code
# Good IaC is self-documenting:
resource "aws_security_group" "web" {
name = "web-tier-sg"
description = "Allow HTTP/HTTPS from internet, all traffic to app tier"
vpc_id = module.vpc.vpc_id
ingress {
description = "HTTPS from internet"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
description = "App tier communication"
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.app.id]
}
tags = {
Name = "web-tier-sg"
Environment = var.environment
Team = "platform"
# No "ManagedBy" tag needed — if it's in Terraform, it's managed by Terraform
}
}Infrastructure as Code is a cultural shift as much as a technical one: infrastructure changes through pull requests, reviewed and approved, then automatically applied. This eliminates the "snowflake servers" problem and gives you confidence that your infrastructure matches your documentation — because they're the same thing.
