Skip to content
ExponaBLOG
TERRAFORM, IAC & CLOUD AUTOMATION · DAY 001 / 100

Why infrastructure as code matters: a step-by-step guide

Clicking in consoles does not scale. Code does.

Expona Admin6 min read

Introduction

We've all been there: a critical service goes down at 2 AM, and the only person who remembers which load balancer rules were changed is on vacation. Or worse, you rebuild a staging environment from scratch and it doesn't match production—but you can't figure out why because the changes live only in someone's terminal history and a half-forgotten Slack thread. Infrastructure as code matters because it transforms infrastructure management from a manual, error-prone process into a reproducible, auditable, versionable discipline. This guide walks you through why IaC is non-negotiable for any team shipping to production, and how tools like Terraform turn infrastructure into something you can reason about, review, and rollback just like application code.

Reproducible Environments

Reproducibility is the foundation of reliability. When infrastructure lives in the console—AWS, GCP, Azure—it becomes a snowflake: unique, fragile, and impossible to recreate with certainty. A single missed click, a changed default, or a security group rule added by someone else and never documented means your staging environment silently drifts from production.

With infrastructure as code, your entire deployment is declared in files. Run Terraform against a fresh account, and you get identical results. No surprises. No "it worked in staging but not production."

Consider a real scenario: your team needs a new staging environment for a client engagement. With manual setup, you'd SSH into bastion hosts, run CLI commands, update documentation (which quickly goes stale), and hope the junior engineer didn't miss a step. With Terraform, you clone your infrastructure repository, point it at a new AWS account, run terraform apply, and walk away. Twenty minutes later, you have an exact replica.

Here's a simplified example:

hcl
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name        = "${var.environment}-vpc"
    Environment = var.environment
  }
}

resource "aws_subnet" "private" {
  count             = length(var.private_subnet_cidrs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "${var.environment}-private-subnet-${count.index + 1}"
  }
}

resource "aws_security_group" "app" {
  name   = "${var.environment}-app-sg"
  vpc_id = aws_vpc.main.id

  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = [aws_vpc.main.cidr_block]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Define your VPC, subnets, and security groups once. Spin up dev, staging, and production with identical topology by changing a single variable file. That reproducibility compounds: every engineer on your team can provision a complete environment without tribal knowledge. Onboarding becomes faster. Debugging becomes easier because your test environment actually matches where the bug was reported.

Reviewable Changes in Pull Requests

Infrastructure changes are code changes. They should be reviewed like code changes. Yet how many teams still apply infrastructure updates by having one person run scripts directly in production, with no peer review, no approval gate, and no record of who approved what?

IaC enables the pull request workflow for infrastructure. Before a single resource is created or modified, teammates see exactly what will change. A security engineer can spot an overly permissive ingress rule. A platform lead can verify cost implications. A junior engineer learns by reviewing the diff.

The diff is concrete:

hcl
resource "aws_rds_instance" "postgres" {
  identifier     = "${var.environment}-db"
  engine         = "postgres"
  engine_version = "15.3"
  instance_class = "db.t3.medium"
  
  allocated_storage = var.allocated_storage
  storage_encrypted = true
  
  db_name  = var.db_name
  username = var.db_user
  password = random_password.db_password.result
  
  backup_retention_period = var.backup_retention_days
  skip_final_snapshot     = var.environment != "production"
  
  tags = {
    Name        = "${var.environment}-postgres"
    Environment = var.environment
  }
}

In a PR, reviewers see: "We're changing instance class from t3.small to t3.medium. Storage encryption is enabled. Backup retention is 30 days in production, 7 in staging." These decisions are explicit. Questions get asked. Alternatives are discussed. Approval is documented. This is how mature teams move.

A common practice: require approval from specific CODEOWNERS before any infrastructure merge. Your Terraform modules for networking go to the platform team. Compute changes go to infrastructure lead. This prevents cowboys from shipping production changes solo, and it creates institutional knowledge as experienced engineers review every change.

History of Every Change in Git

Git is your audit trail. Every resource creation, every security group rule, every database backup policy—all tracked with timestamps, author, and rationale in commit messages.

This matters enormously in production incidents. A service is erroring with permission denied. You git log your IAM policy and see: three months ago, a contractor added a policy that was never documented. Two months ago, someone removed an inline policy without adding it to the managed policy. Six weeks ago, a senior engineer added the right permissions but to the wrong role. Git blame shows all of it. You can cherry-pick the correct commit, or understand why a permission exists at all.

Beyond incidents, it matters for compliance. Financial services, healthcare, e-commerce—regulators want to know who changed what, when, and why. Git provides that record. A commit message saying "JIRA-1234: Allow read access to S3 bucket for new report feature" beats "Someone probably did this for a reason" every time.

Your history also enables safe rollbacks. If a deployment causes unexpected behavior, you don't troubleshoot blindly. You revert the commit. Your infrastructure goes back to the last known good state. Apply this discipline to infrastructure and you dramatically reduce mean time to recovery.

Faster Recovery After Mistakes

Mistakes happen. A security group accidentally becomes public. A database backup policy gets deleted. A subnet CIDR overlaps with another team's infrastructure. In manual environments, recovery is manual: reproduce the exact steps, hope you remember all the details, pray you don't miss anything.

With IaC, recovery is a git revert and a terraform apply. Minutes instead of hours. No guesswork. No hidden configuration drifting unnoticed.

Picture this: a team member applies a Terraform change that removes availability zone redundancy to save costs. The change merges. Five minutes later, that AZ fails. With IaC, you run one command:

hcl
git revert HEAD
terraform plan
# Review the plan
terraform apply

Your infrastructure snaps back to the multi-AZ configuration. The team discusses why the cost-saving change had a flaw (hidden blast radius), improves the Terraform code to make that mistake harder to make again (perhaps with a local validation or a required approval from ops), and redeploys thoughtfully.

Without IaC? You're scrolling through AWS console screenshots, asking "Which AZ were we in before?" and manually recreating configuration from memory.

This recovery speed compounds over time. Early in a project, IaC feels like overhead. Six months in, when you've prevented three incidents through rapid rollback and caught two security issues in code review before they reached production, the time investment pays dividends.

Common Mistakes

Storing secrets in Terraform code or state files. Never commit API keys, database passwords, or certificates to Git. Use AWS Secrets Manager, HashiCorp Vault, or similar. Reference them in Terraform, but don't embed them.

Manual changes alongside Terraform. If your team runs CLI commands or console clicks on resources Terraform manages, your state drifts. Everyone must use the Terraform workflow. No exceptions, no shortcuts. Enforce this with IAM policies that block direct console/CLI access to critical resources.

Skipping the plan step. Always run terraform plan before apply. Review the output. Ask: does this match my intention? I've seen teams apply changes without reviewing the plan, only to discover they're recreating a database (and losing data) because they changed an immutable attribute.

Monolithic state files. Don't put your entire infrastructure in one Terraform configuration. Split by environment, by team, or by concern. Smaller state files mean smaller blast radius, faster planning, and fewer merge conflicts.

Key Takeaways

  • Infrastructure as code transforms infrastructure from snowflake to reproducible system. Spin up identical environments in minutes.
  • PR-based workflows for infrastructure enable peer review, catch bugs early, and create institutional knowledge through documented decisions.
  • Git history provides audit trail for compliance, incident response, and understanding the "why" behind configuration.
  • Mistakes in IaC environments are recoverable via git revert and terraform apply—a process measured in minutes, not hours.
  • Establish team norms: no manual infrastructure changes, always review plans, separate state files, never commit secrets.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles