Terraform Cheatsheet - Infrastructure as Code Command Reference
The Terraform rhythm is fixed: plan to see the diff, apply to commit, destroy with real care. Everything else follows from the state file, which records how declared resources map to real ones and acts as the source of truth for the tool. Lose the state and the next plan proposes recreating everything; drift between state and reality can trigger unexpected replacement. That is why shared workspaces belong on a remote backend with locking enabled. Sections cover the core workflow, state management, variables and outputs, modules, remote backends, advanced features, and debugging. A few rules hold up in practice: pass secrets as variables or mark them sensitive rather than inlining them, keep state out of Git because it carries credentials, and run terraform fmt so reviews stay readable. When something looks wrong, state list and state show come first, and refresh-only is the smallest tool for correcting drift. Import turns an existing hand-built resource into a managed one without destroying anything, which is the safe first step when adopting Terraform on a live stack.
Typical Use Case
For engineers responsible for infrastructure as code in the cloud. The familiar rhythm is init, plan, apply, but the time actually goes into four other things this sheet covers: state management, where list, show, mv, rm and import are the tools for investigating drift and mismatches between config and reality, plus taint to force replacement; remote backends, with S3 for state and DynamoDB for locking, which is what makes team and concurrent apply safe; choosing between count and for_each for bulk creation; and debugging through the console, TF_LOG and output logs. Workspace covers keeping staging and production state apart.
Core Workflow 7
terraform initterraform planterraform applyterraform destroyterraform fmtterraform validateterraform apply -auto-approveState Management 9
terraform state listterraform state show <resource>terraform state mv <src> <dst>terraform state rm <resource>terraform state pullterraform state pushterraform import <address> <id>terraform taint <resource>terraform untaint <resource>Variables & Outputs 8
variable "region" {
type = string
default = "us-east-1"
}var.regionTF_VAR_region=us-west-2terraform plan -var="region=us-west-2"output "ip" {
value = aws_instance.web.public_ip
}terraform outputterraform output -jsonlocals {
name = "${var.prefix}-web"
}Modules 6
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
}module "vpc" {
source = "./modules/vpc"
}module "vpc" {
source = "git::https://example.com/vpc.git"
}module.vpc.vpc_idterraform init -upgradeterraform getRemote Backends 6
terraform {
backend "s3" {
bucket = "my-tf-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
}
}terraform {
backend "s3" {
dynamodb_table = "tf-lock"
}
}data "terraform_remote_state" "db" {
backend = "s3"
config = {
bucket = "shared-state"
key = "db/terraform.tfstate"
}
}data.terraform_remote_state.db.outputs.db_endpointterraform init -reconfigureterraform init -migrate-stateAdvanced Features 9
terraform workspace new stagingterraform workspace listterraform workspace select stagingresource "aws_instance" "web" {
for_each = var.subnets
}resource "aws_instance" "web" {
count = var.instance_count
}depends_on = [aws_s3_bucket.data]lifecycle {
create_before_destroy = true
}lifecycle {
prevent_destroy = true
}lifecycle {
ignore_changes = [tags]
}Debugging 8
terraform consoleterraform graph | dot -Tpng > graph.pngterraform showterraform show -jsonTF_LOG=DEBUG terraform applyTF_LOG=TRACE terraform planTF_LOG_PATH=./tf.log terraform applyexport TF_LOG=WARNParameter matrix
| Parameter | Effect | Example |
|---|---|---|
init | Initialize the working directory and download providers and modules | terraform init |
init -upgrade | Upgrade modules to the latest version currently allowed | terraform init -upgrade |
init -reconfigure | Reconfigure the backend (migrate state) | terraform init -reconfigure |
init -migrate-state | Migrate the state file to a new backend | terraform init -migrate-state |
plan -var="k=v" | Pass variable values on the command line | terraform plan -var="region=us-west-2" |
apply -auto-approve | Skip manual confirmation; for CI use | terraform apply -auto-approve |
validate / fmt | Validate config consistency / unify code formatting | terraform fmt |
state list / show | List resources in state / show details of a single resource | terraform state list |
state mv <src> <dst> | Move a resource's address in state | terraform state mv aws_instance.a aws_instance.b |
state rm <res> | Remove from state only; does not destroy the real resource | terraform state rm aws_s3_bucket.old |
import <addr> <id> | Bring existing resources under Terraform management | terraform import aws_instance.web i-0123456789 |
taint / untaint <res> | Mark as damaged for rebuild next time / unmark | terraform taint aws_instance.web |
output / output -json | View output values / view them as JSON | terraform output -json |
workspace new/select/list | Create / switch / list workspaces | terraform workspace select staging |
count / for_each | Create resources in bulk by count or by collection | count = var.instance_count |
console | Interactive console for testing expressions | terraform console |
TF_LOG / TF_LOG_PATH | Set the log level (TRACE/DEBUG/INFO/WARN/ERROR) and output path | TF_LOG_PATH=./tf.log terraform apply |
Common pitfalls
Symptomapply reports the state is already locked / lock timeout, and people block each other when operating at the same time
CauseThe remote backend has no state locking configured, or uses S3 without a lock table.
FixAdd dynamodb_table = "tf-lock" inside backend "s3", then run terraform init -migrate-state.
Symptomplan shows a large number of differences that were not caused by this change
CauseRemote resources were modified by hand, so the local state drifts from reality.
FixFirst confirm with terraform state list and terraform state show <resource>, then run terraform apply to realign state with config; for major differences, archive and review first with terraform plan -out.
SymptomAfter renaming a resource, Terraform wants to "destroy the old and create a new" one, but the old resource cannot be deleted
CauseThe resource address changed, so Terraform cannot tell that the two are the same thing.
FixUse terraform state mv <old address> <new address> to claim the resource under the new address; do not force it through by copying the config.
SymptomThe resource was already deleted by hand, but apply reports a creation conflict
CauseThe state still records this now-nonexistent resource.
FixUse terraform state rm <resource> to remove it from state (this command does not delete the real resource), then apply again.
SymptomThe output exposes sensitive information such as database passwords and keys
CauseThe state file itself contains all attributes of the resources, including sensitive fields; committing it to Git spreads them.
FixPut sensitive values in a variable without a default and inject them via environment variables or CI secrets; add the state directory to .gitignore and enable encryption on the remote backend.
Symptomapply hangs waiting for confirmation in CI and the pipeline times out
CauseThe auto-approve flag was not added, so the command stops at interactive confirmation.
FixAlways use terraform apply -auto-approve in non-interactive environments, and make sure the pipeline has completed the init and plan review steps.
SymptomAfter switching count to for_each (or vice versa), Terraform wants to destroy and rebuild the entire batch of resources
CauseThe two use different resource address forms, so switching is equivalent to changing identity.
FixBefore switching, use terraform state mv to migrate existing resource addresses where possible; or, when accepting the rebuild cost, evaluate the downtime window first.
Troubleshooting
1Confirm exactly which resources are in the local state
terraform state listThe resource addresses in the output are the first-hand evidence for troubleshooting "missing resources / extra resources".
2View the actual state attributes of a single resource
terraform state show aws_instance.webUse it to compare config with the real resource and locate the source of drift.
3Output state as JSON for grep/jq analysis
terraform show -jsonGood for counting resources, comparing IDs in scripts, or diffing against plan results.
4Interactively verify whether an expression evaluates correctly
terraform consoleEnter expressions like var.region or local.name and see the result directly—much faster than editing and running plan.
5Turn on detailed logging and write it to disk
TF_LOG=DEBUG TF_LOG_PATH=./tf.log terraform planThe log shows the API requests and responses the provider actually sends—key to troubleshooting "the external system cannot change the resource".
6Expose potential syntax problems with a code-format check
terraform fmt -check -diffUsed as a CI gate; the diff output is the set of files that do not conform to the standard.
Command Examples
Review the diff with plan before applying
terraform plan -out=tfplan
terraform show tfplanWith plan -out saved to a file, you can review it with terraform show tfplan, then run terraform apply tfplan to execute the same plan—guaranteeing that what runs is the version you reviewed.
Output
# aws_instance.web will be updated in-place
~ resource "aws_instance" "web" {
~ instance_type = "t2.micro" -> "t3.medium"
}
Plan: 0 to add, 1 to change, 0 to destroy.Detect drift between reality and state
terraform plan -detailed-exitcode
echo $?Exit codes: 0 = no changes, 1 = error, 2 = changes that are non-destructive. This convention lets it be used directly in CI for infrastructure drift detection.
Output
2
Make Terraform forget a resource
terraform state rm aws_instance.web
terraform planstate rm only changes state without touching the real resource, and the following plan then shows a rebuild—exactly "delete and recreate" rather than accidental deletion. Using destroy directly really does delete the machine.
Output
# aws_instance.web will be created
+ resource "aws_instance" "web" {
+ id = "i-0abc123"
}
Plan: 1 to add, 0 to change, 0 to destroy.Common Pitfalls
- Always review plan before apply, and never pass -auto-approve to a plan containing destroys without reading it.
- State contains sensitive data and must not be committed; production needs a remote backend with locking enabled.
- terraform destroy removes every managed resource including databases; use -target or remove the resource from config to drop one.
- Changing an for_each key destroys and recreates the whole resource — keys must be stable identifiers.
- Run terraform workspace show before acting; operating on the wrong workspace between prod and staging is a real incident.
- state rm makes a hand-created resource look missing and get recreated — use it to correct drift, not to hide it.
Tips
- terraform plan never modifies resources — use it freely
- Keep secrets in variables; never hardcode them in .tf files
- Use terraform fmt to standardize formatting — essential for teams
- The state file holds sensitive data; never commit it to Git
- Use a remote backend (e.g. S3 + DynamoDB) for collaboration and state locking
FAQ
Why run terraform plan separately from apply?
plan computes and displays the diff without touching anything; apply performs the create, change or destroy. Running plan first inserts a human checkpoint, which matters most for changes that rebuild or delete resources. A stronger workflow is to save the plan to a file and re-apply that exact plan, guaranteeing you execute precisely what you reviewed.
What is the difference between terraform state rm and terraform destroy?
destroy actually deletes the cloud resource and drops it from state; state rm only removes the entry from the state file while leaving the real resource running. They solve different problems. When state has drifted from reality, state rm makes Terraform forget the resource so it does not plan a destroy-then-recreate. Use destroy only when you actually want to reclaim the infrastructure.
When should I switch Terraform workspaces?
Use one workspace per environment — staging, prod — created with terraform workspace new staging, switched with select, listed with list. Note that all workspaces share the same backend state file, so you must ensure resource names do not collide across them. Because workspaces are easy to get wrong, always run terraform workspace show before applying to confirm which environment you are actually in.
Should I use count or for_each to create resources in bulk?
Use count when the instances are identical and only the quantity differs; use for_each when each instance needs different configuration, such as its own subnet or size. A crucial caveat: changing an for_each key destroys and recreates that entire resource, so design keys to be stable identifiers. Likewise, reordering a list used with count shifts indices and triggers recreation.
Should the Terraform state file be committed to Git?
No. State records the mapping to real resources, contains sensitive data, and produces painful merge conflicts. Configure a remote backend (backend "s3" or similar) so it lives in object storage or Terraform Cloud, keeping only configuration in the repository. Teams should also enable state locking — a DynamoDB lock for S3, or Terraform Cloud — or two concurrent applies will overwrite each other.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us