Cheat sheet — Infrastructure as Code (Terraform / OpenTofu)¶
Companion to Module 02 — Infrastructure as Code · CC BY 4.0 — print it, pin it, share it.
Last reviewed: 2026-07
tofuis a drop-in forterraform— swap the binary name and every command below still works.
The core lifecycle¶
terraform init # download providers, set up .terraform/ + lockfile
terraform fmt # canonicalise formatting (fmt -recursive for subdirs)
terraform validate # config is internally consistent (no cloud calls)
terraform plan -out=tf.plan # compute the diff; SAVE it so apply runs exactly this
terraform apply tf.plan # apply the saved plan — no re-prompt, no surprise diff
terraform destroy # tear it all down (plan the destroy first: -out)
- Always
plan -outthenapply <file>. A bareterraform applyre-plans at apply time — what you reviewed and what runs can differ. The saved plan is the thing you reviewed.
Read the plan-diff¶
+ create # new resource
~ update in-place
-/+ replace # DESTROY then create — the dangerous one; read WHY it forces replacement
- destroy
terraform plan # human-readable diff
terraform show -json tf.plan # machine-readable — feed to a policy scanner / jq
terraform plan -target=aws_s3_bucket.logs # scope the plan to one resource (escape hatch)
State¶
terraform state list # every resource Terraform tracks
terraform state show aws_s3_bucket.logs # one resource's tracked attributes
terraform state rm aws_s3_bucket.logs # forget a resource (does NOT delete it)
terraform state mv <old.addr> <new.addr> # rename/move without destroy-recreate
terraform refresh # reconcile state with real infra (or plan -refresh-only)
Variables, outputs, workspaces¶
terraform plan -var 'region=us-east-1' # single var
terraform plan -var-file=prod.tfvars # a file of vars (auto-loaded: *.auto.tfvars)
terraform output # all outputs
terraform output -raw bucket_name # one value, unquoted — pipe into a script
terraform workspace new staging && terraform workspace select staging
Minimal config shape¶
terraform {
required_version = ">= 1.6"
required_providers {
local = { source = "hashicorp/local", version = "~> 2.5" }
}
}
resource "local_file" "note" {
filename = "${path.module}/hello.txt"
content = "reproducible, reviewable, in git"
}
Using modules (reusable building blocks)¶
A module is a folder of .tf files you call from elsewhere — the unit of reuse. The root config
you run apply in is itself a module; anything it calls is a child module.
module "vpc" {
# source is REQUIRED and picks where the module comes from:
source = "terraform-aws-modules/vpc/aws" # public Terraform Registry (namespace/name/provider)
version = "~> 5.8" # version is ONLY valid for registry sources — pin it
# inputs: everything the child module declares as `variable` becomes an argument here
name = "prod"
cidr = "10.0.0.0/16"
}
module "local_thing" {
source = "./modules/thing" # local path — no version arg; re-init if you add/move it
}
# git source: source = "git::https://github.com/org/repo.git//subdir?ref=v1.4.0"
# consume a child module's outputs elsewhere in your config:
resource "aws_instance" "web" {
subnet_id = module.vpc.private_subnets[0] # module.<name>.<output>
}
terraform init # ALSO fetches/updates modules into .terraform/modules
terraform get -update # re-fetch modules only (after bumping a source/version)
versionworks only for registry modules, not local/git sources — pin git with?ref=<tag>.- Adding, moving, or re-sourcing a module means re-run
init(orget) beforeplan, or Terraform won't see it. - Keep child modules small and input-driven: expose
variables in,outputs out — no hardcoded account IDs or regions inside the module.
Gotchas worth remembering¶
terraform.tfstateis a bundle of plaintext secrets — it stores every attribute, including passwords and private keys, unencrypted. Never commit it (.gitignoreit), never paste it in a ticket. Use a remote backend (S3 + DynamoDB lock, or TF Cloud) for anything shared.- Review the plan-diff, never the apply. The same
applythat builds 100 resources can destroy -/+ replaceon a database is a data-loss incident hiding in a green diff — read why.- Commit
.terraform.lock.hcl. It pins provider hashes so every teammate and CI runner resolves the exact same provider versions — reproducibility depends on it. state rm≠ delete,destroy= delete.state rmmakes Terraform forget a live resource (it keeps running);destroytears the real thing down. Confusing them either orphans infra or nukes it.- Pin providers with
~>, not open ranges.version = "5.0.0"or~> 5.0— an unpinned provider can pull a breaking change on the nextinitand rewrite your plan. fmt+validatebelong in CI, before plan — cheap, offline, and they catch the typo before it costs a cloud round-trip.
Comments
Sign in with GitHub to comment. Choose the type: Feedback (errors or suggestions on this page) · Hints (help for fellow learners — no spoilers) · General (anything else).