Infrastructure as Code (IaC) means defining servers, networks, and databases in version-controlled configuration files instead of clicking through a cloud console — and Terraform became the tool most teams default to for it, for specific, non-arbitrary reasons.
What problem IaC actually solves
Before IaC, infrastructure was configured by hand through a console, or via ad hoc scripts — meaning there was no single source of truth for what your infrastructure actually looked like, no code review for infrastructure changes, and no reliable way to reproduce an environment (staging that's supposed to mirror production, but doesn't, because someone clicked a setting six months ago and never documented it).
# main.tf
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "web-server"
}
}This file, checked into Git, is the definition of that server. Change the file, run terraform apply, and the infrastructure updates to match — the same review, diff, and audit trail you already use for application code now applies to infrastructure too.
Why Terraform specifically, over cloud-native tools
Every major cloud has its own native IaC tool — AWS has CloudFormation, Azure has ARM/Bicep, GCP has Deployment Manager. Terraform (from HashiCorp) is cloud-agnostic by design, using providers as a plugin layer:
provider "aws" {
region = "us-east-1"
}
provider "cloudflare" {
api_token = var.cloudflare_token
}One Terraform configuration can define AWS resources, Cloudflare DNS records, a Datadog monitor, and a GitHub repository setting, all in the same declarative language, tracked in the same state. A team using CloudFormation for AWS infrastructure still needs a completely different tool and syntax the moment they touch a non-AWS service — Terraform avoids that fragmentation.
| Aspect | Terraform | Cloud-native (CloudFormation, ARM, etc.) |
|---|---|---|
| Provider scope | Multi-cloud, via a plugin/provider model | Locked to one cloud's own services |
| Day-one feature support | Can lag behind the newest cloud features | Always current with its own platform |
| Best for | Multi-cloud or multi-SaaS teams | Single-cloud teams wanting day-one access to new features |
The core mental model: state
Terraform keeps a state file recording what it believes currently exists. Every terraform plan compares your configuration against that state (and the actual cloud provider) to compute exactly what needs to change:
terraform plan # shows what WOULD change, without changing anything
terraform apply # actually makes the changeterraform plan is what makes IaC trustworthy in practice — you see the exact diff (2 resources to add, 1 to change, 0 to destroy) before anything touches real infrastructure, the same review-before-merge instinct developers already have for code.
Where teams get burned
Manual changes outside Terraform. If someone changes a resource by hand in the cloud console, Terraform's state no longer matches reality — the next plan either shows a confusing diff trying to "fix" the manual change, or (worse) Terraform overwrites it without anyone realizing. The rule that makes IaC actually work: once a resource is managed by Terraform, all changes to it go through Terraform, no exceptions.
State file handling. The state file needs to be shared safely across a team (typically stored remotely — S3 with locking via DynamoDB, or Terraform Cloud) rather than sitting on one person's laptop. Losing it, or two people applying changes concurrently without locking, are the most common ways teams get burned early on.
The honest tradeoff
Terraform's cloud-agnostic design is also its main criticism: it's a generic abstraction over each provider's API, which means it sometimes lags behind a cloud's newest features (a brand-new AWS service might not have full Terraform provider support on day one) compared to a cloud-native tool that's always current with its own platform. For teams committed to a single cloud who want day-one access to every new feature, that's a real tradeoff. For most teams — especially anyone using more than one cloud or SaaS provider — the consistency wins.
Modules: Terraform's reuse mechanism
A Terraform module is a reusable, parameterized package of configuration — the same infrastructure pattern (a standard VPC layout, a standard web-app deployment shape) defined once and instantiated multiple times with different inputs, rather than copy-pasted per environment:
module "production_vpc" {
source = "./modules/vpc"
cidr_block = "10.0.0.0/16"
env = "production"
}
module "staging_vpc" {
source = "./modules/vpc"
cidr_block = "10.1.0.0/16"
env = "staging"
}Both instantiations reuse the exact same underlying module code, differing only in the inputs passed — a fix or improvement to the module's internal logic (adding a missing tag, correcting a security group rule) propagates to every environment using it on the next apply, instead of needing to be hand-applied to each environment's separately copy-pasted configuration.
Terraform Cloud/Enterprise vs. self-managed state
Beyond a bare S3-plus-DynamoDB remote backend, HashiCorp's own Terraform Cloud (and the self-hosted Enterprise version) adds a managed layer on top: remote plan/apply execution (so apply runs in a consistent environment, not on whoever's laptop happens to run it), a UI for reviewing plans before approval, and built-in state locking without configuring DynamoDB manually. For a team already comfortable managing their own S3/DynamoDB backend and CI-driven applies, it's not strictly necessary — but for teams wanting a more guided, less self-assembled workflow around exactly the failure modes described above (state handling, concurrent applies, plan review), it's worth evaluating directly against a self-managed setup rather than assumed to be unnecessary overhead.
The same declarative discipline Terraform brings to raw infrastructure extends naturally into what runs on top of it — Kubernetes ConfigMaps and Secrets are the equivalent pattern one layer up, and a real-world 3-tier AWS deployment is exactly the kind of multi-resource setup where hand-clicking through a console stops being viable.
Common mistakes
- Making a manual change in the cloud console "just this once" to fix something urgently, then forgetting to reconcile it back into the Terraform configuration — the next plan either fights the manual change or silently reverts it.
- Storing the state file locally (or committing it to git) instead of a remote backend with locking. A local state file can't safely be shared across a team, and committing it to git risks leaking sensitive values the state file often contains (like generated passwords).
- Running
terraform applywithout reading the plan output first, especially in automation — aplanshowing an unexpected "destroy" on a resource is exactly the kind of thing this workflow exists to catch before it happens, not after. - Writing one enormous Terraform configuration for an entire organization's infrastructure instead of splitting it into logical, independently-applied modules — a mistake in one part of a monolithic config can block or risk unrelated infrastructure during every single apply.
Related reading
- CI/CD Pipelines Explained: From Commit to Production — shares tags: devops, cloud (same category).
- Understanding Cloud Cost Optimization Basics — shares tags: cloud, devops.
- Kubernetes ConfigMaps and Secrets: A Practical Guide — shares tags: devops, cloud.
- Docker Multi-Stage Builds: A Step-by-Step Tutorial — shares tags: devops, cloud.
- Deploying a 3-Tier Application on AWS with Public and Private Subnets — shares tags: devops, cloud.