Terraform is the dominant infrastructure-as-code tool. You declare desired infrastructure in HCL (HashiCorp Configuration Language); Terraform reconciles the declaration against actual cloud state and applies changes. The model is declarative: describe what you want, not how to get there.
This page covers the core concepts and the patterns that make Terraform sustainable at scale.
A Terraform provider implements the API for a specific platform: AWS, Azure, GCP, Kubernetes, GitHub, Datadog, etc. The provider knows how to create, read, update, and delete each resource type.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-west-2"
}
Providers are versioned. Pin them; floating versions break things.
A resource declares one piece of infrastructure:
resource "aws_s3_bucket" "example" {
bucket = "my-bucket-name"
tags = {
Environment = "production"
}
}
Terraform creates the bucket if it doesn't exist; updates it if the declaration changes; deletes it if removed from config.
Terraform tracks the mapping from declared resources to actual cloud resources in a state file. State is the source of truth for what Terraform manages.
State storage is critical:
State must be shared and locked. Two engineers running apply simultaneously without locking corrupt the state.
terraform plan # show what would change
terraform apply # actually change it
plan is the safety net. Always review before applying. CI/CD typically requires plan output in PRs before apply is allowed.
Reusable bundles of resources:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
}
The community has high-quality modules for common patterns. Use them instead of writing from scratch.
Terraform workspaces (or separate state files) isolate environments. Common: dev, staging, production each with its own state. The same code applies; the variables differ.
Larger projects compose modules:
infrastructure/
├── modules/
│ ├── vpc/
│ ├── ecs-cluster/
│ └── rds-postgres/
└── environments/
├── dev/
│ └── main.tf (uses the modules)
└── prod/
└── main.tf
Modules encode patterns; environments wire them together.
Modules declare inputs (variables) and outputs:
variable "instance_type" {
type = string
default = "t3.medium"
}
output "instance_id" {
value = aws_instance.example.id
}
Outputs from one module become inputs to another. This is the wiring.
Don't put secrets in state files. Use:
"Run this script on the instance" — Terraform handles infrastructure, not configuration. For that, use Ansible, Chef, cloud-init, or container images.
Terraform creates infrastructure; deploying application code on it is a separate concern. CI/CD tools handle that.
Terraform tracks what it manages; drift outside Terraform (someone clicks in the console) requires terraform plan to detect, and the response is awkward.
IaC in real programming languages (TypeScript, Python, Go, C#). More flexible than HCL; familiar control flow. Trade-off: less standardized; smaller ecosystem; more code complexity for simple cases.
Cloud Development Kit — code that generates CloudFormation. AWS-specific. For AWS-only shops, sometimes preferred over Terraform.
AWS's native IaC. Mature; tightly integrated with AWS. Less popular than Terraform because CloudFormation's evolution is slow and Terraform's multi-cloud story is appealing.
For most multi-cloud or even single-cloud usage, Terraform remains the default.