Skip to content
Back to Blog
DevOps

Infrastructure as Code: Principles, Tools, and Workflows

Master IaC principles with Terraform, Ansible, and Pulumi: idempotency, state management, versioning, and team collaboration workflows.

Jan 2026
15 min read

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 Imperative
TEXT
Imperative (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)
2. Idempotency
HCL
# 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 replaces
3. Version Control Everything
TEXT
Your 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)
4. Immutable Infrastructure
TEXT
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 version

Terraform: State Management

HCL
# 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

BASH
# 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

TEXT
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

BASH
# 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 test

GitOps for Infrastructure

YAML
# .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.tfplan

Documentation as Code

HCL
# 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.