Understanding Terraform modules
HCL, state, plan, apply — the four words you'll repeat forever.
What a module gives you, why state files are the database of your infrastructure, and how plans turn into safe deploys.
Declarative infrastructure.
Terraform reads .tf files describing the desired state of cloud resources, compares against actual state, and produces a plan of changes. Apply the plan; the cloud matches the code. The flow — write HCL, terraform plan, terraform apply — is the entire model. The hard part is the state file in the middle.
The state file.
terraform.tfstate is the database of every resource Terraform manages — IDs, attributes, dependencies. Without state, Terraform couldn't know thataws_instance.web is the EC2 instance with ID i-abc123. State should live in a remote backend (S3 + DynamoDB lock, or Terraform Cloud) — never in git, where two engineers running plan at once would corrupt it. Locking on the backend is what prevents that.
Modules — the reuse unit.
A module is a folder of .tf files with declared inputs and outputs. Call it from another configuration: module "vpc" { source = "./modules/vpc"; cidr = "10.0.0.0/16" }. The module encapsulates a chunk of infrastructure — a VPC with subnets, an EKS cluster, an RDS instance. Public modules on the Terraform Registry handle the most common patterns. Internal modules are how teams enforce standards across environments without copy-pasting HCL.
Plan, then apply.
terraform plan prints the diff between current state and desired state: "add 3 resources, change 2, destroy 1". Review carefully. terraform applyexecutes the plan. The plan is the moment to catch bugs — a unintended destroy is obvious in a plan and easy to miss after the fact. CI should run plan on every PR, post the output as a comment, require human approval before apply.
A worked module.
A "static site" module: takes a domain name as input, produces a CloudFront distribution + S3 bucket + Route53 record + ACM certificate as managed resources, and outputs the distribution URL. 60 lines of HCL once; called from any number of stacks. Adding a new static site is now 5 lines: module "blog" { source = "./modules/static-site"; domain = "blog.example.com" }. The encapsulation pays back in months of saved boilerplate.
Static-site module
4 resources + 1 output
One module call → CloudFront + S3 + DNS + cert.
module "blog" { domain = "blog.example.com" }
= 5 lines per new site
Drift, imports, refactors.
Drift: someone changed a resource in the console without updating Terraform; next plan shows the change. Either undo the manual change, or update the .tf to match. Import:terraform import aws_instance.web i-abc123 tells Terraform to take over an existing resource. Refactor: moved { from = aws_s3_bucket.old; to = aws_s3_bucket.new }renames in state without destroying. The state file is fragile in early projects; treat changes to it like database migrations.