You have been clicking around the AWS console, manually creating EC2 instances, S3 buckets, and security groups. It works, until it does not. Someone changes a setting you do not notice. You cannot reproduce your staging environment. A new team member asks "how do I set this up?" and you realize the answer is a 45-minute screen share.
Infrastructure as Code solves this. Terraform is the most popular tool for the job, and it is simpler than you think.
What Is Infrastructure as Code
Infrastructure as Code (IaC) means defining your cloud resources in configuration files instead of clicking through web consoles. These files are versioned in Git, reviewed in pull requests, and applied automatically.
The benefits are immediate:
- Reproducibility. Run the same config and get the same infrastructure every time.
- Documentation. Your infrastructure is described in code that anyone can read.
- Collaboration. Changes go through code review. No more rogue manual changes.
- Recovery. Accidentally delete something? Re-apply your config and it comes back.
Why Terraform
Terraform is cloud-agnostic. The same workflow works for AWS, GCP, Azure, Cloudflare, Vercel, and hundreds of other providers. You learn one tool and one language (HCL) and can manage infrastructure across any platform.
Terraform uses a declarative approach. You describe what you want, not how to get there. Terraform figures out the steps to reach that state.
Core Concepts
Providers connect Terraform to cloud platforms:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}Resources are the things you create:
resource "aws_s3_bucket" "website" {
bucket = "my-awesome-website"
}
resource "aws_s3_bucket_website_configuration" "website" {
bucket = aws_s3_bucket.website.id
index_document {
suffix = "index.html"
}
error_document {
key = "404.html"
}
}State is how Terraform tracks what it has created. By default, it is stored in a local terraform.tfstate file. For teams, you store it remotely (more on this later).
The Terraform Workflow
There are three commands you will use constantly:
# Preview what Terraform will do
terraform plan
# Apply the changes
terraform apply
# Destroy everything (careful!)
terraform destroyterraform plan is your safety net. It shows you exactly what will be created, changed, or destroyed before anything happens. Always read the plan before applying.
Building Your First Infrastructure
Let us build something real: a static website hosted on AWS S3 with CloudFront CDN.
File structure:
infrastructure/
main.tf # Main resources
variables.tf # Input variables
outputs.tf # Output values
terraform.tfvars # Variable values (gitignored if sensitive)variables.tf:
variable "domain_name" {
description = "The domain name for the website"
type = string
}
variable "environment" {
description = "Deployment environment"
type = string
default = "production"
}main.tf:
resource "aws_s3_bucket" "website" {
bucket = "${var.domain_name}-${var.environment}"
tags = {
Environment = var.environment
ManagedBy = "terraform"
}
}
resource "aws_s3_bucket_public_access_block" "website" {
bucket = aws_s3_bucket.website.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_cloudfront_distribution" "website" {
origin {
domain_name = aws_s3_bucket.website.bucket_regional_domain_name
origin_id = "S3-${aws_s3_bucket.website.id}"
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.website.cloudfront_access_identity_path
}
}
enabled = true
default_root_object = "index.html"
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "S3-${aws_s3_bucket.website.id}"
viewer_protocol_policy = "redirect-to-https"
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
}
resource "aws_cloudfront_origin_access_identity" "website" {
comment = "OAI for ${var.domain_name}"
}outputs.tf:
output "cloudfront_url" {
description = "The CloudFront distribution URL"
value = aws_cloudfront_distribution.website.domain_name
}
output "bucket_name" {
description = "The S3 bucket name"
value = aws_s3_bucket.website.id
}Run it:
terraform init # Download providers
terraform plan # Preview changes
terraform apply # Create resourcesTerraform will show you the plan and ask for confirmation. Type yes and your infrastructure is created.
Modules
Once your config grows, you will want to reuse pieces. Modules are Terraform's answer:
module "website" {
source = "./modules/static-website"
domain_name = "mysite.com"
environment = "production"
}
module "staging_website" {
source = "./modules/static-website"
domain_name = "mysite.com"
environment = "staging"
}The Terraform Registry has thousands of community modules for common patterns. Use them for standard infrastructure (VPCs, EKS clusters, RDS databases) and write custom modules for your specific needs.
Remote State
For teams, local state files are a disaster. Two people running terraform apply simultaneously will corrupt your state. Use remote state:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "production/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}The DynamoDB table provides state locking — only one person can apply changes at a time. This is not optional for teams. Set it up from day one.
Common Pitfalls
1. Not using terraform plan before apply. The plan is your code review. Read it every time.
2. Storing state locally in a team. This leads to state conflicts and data loss. Use remote state with locking.
3. Hardcoding values. Use variables for anything that changes between environments. Use locals for computed values.
4. Ignoring the state file. Never manually edit terraform.tfstate. If state gets out of sync, use terraform import to bring existing resources under management.
5. Not tagging resources. Always tag with ManagedBy = "terraform" and Environment. You will thank yourself when you need to find resources later.
6. Giant monolithic configs. Split by concern. Networking, compute, storage, and monitoring should be separate state files (or at least separate modules).
Terraform vs Alternatives
Pulumi — Uses real programming languages (TypeScript, Python, Go) instead of HCL. Better if your team struggles with HCL's declarative nature. Worse for readability — infrastructure configs should be boring and predictable.
AWS CloudFormation / Azure Bicep / Google Deployment Manager — Cloud-specific. Deeper integration with their respective platforms but locks you into one cloud. If you are all-in on one cloud and plan to stay, these are viable.
AWS CDK / CDKTF — Write infrastructure in TypeScript/Python that compiles to CloudFormation or Terraform. The abstraction layer adds complexity. Useful for very dynamic infrastructure definitions.
For most teams, Terraform is the right default. It is cloud-agnostic, has the largest ecosystem, and HCL is simple enough that non-DevOps engineers can read and contribute to it.
Getting Started Today
- Install Terraform:
brew install terraform(or download from hashicorp.com) - Pick one small piece of infrastructure you manage manually
- Write the Terraform config for it
- Use
terraform importto bring the existing resource under management - Verify with
terraform plan— it should show no changes - Commit to Git and set up remote state
Start small. Do not try to terraform your entire infrastructure in a week. Pick one service, get comfortable, and expand from there. Within a month, you will wonder how you ever managed infrastructure without it.