Terraform Cheatsheet
Commands
Use this Terraform reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
The Command Set
| Command | What it does |
|---|---|
init | download providers and modules, configure the backend |
validate | check syntax, types and internal consistency, no remote calls |
fmt | rewrite files to canonical style |
plan | compute and show the diff |
apply | make the diff real |
destroy | shorthand for apply -destroy |
test | run .tftest.hcl test files (1.6+) |
query | run .tfquery.hcl List Resource queries (1.14+) |
show | render state or a saved plan file |
output | print root module output values |
state | inspect and surgically edit state |
console | interactive expression evaluator |
graph | emit the dependency graph |
providers | inspect provider requirements, schemas and the lock file |
get | download modules only |
workspace | manage named state files |
force-unlock | release a stuck state lock |
import | write an existing object into state |
login / logout | credentials for HCP Terraform and private registries |
version | version plus provider versions, and a release check |
Global options come before the subcommand: terraform -chdir=envs/prod apply. Everything else comes after.
init
terraform init terraform init -upgrade # re-resolve providers AND modules terraform init -input=false # never prompt, for CI terraform init -backend-config=prod.s3.hcl # partial backend configuration terraform init -reconfigure # new backend, DISCARD the old link terraform init -migrate-state # new backend, COPY the state over terraform init -lockfile=readonly # fail if the lock file would change
| Flag | Description |
|---|---|
-upgrade | re-resolve provider and module versions within the constraints |
-reconfigure | ignore the existing backend record and start fresh |
-migrate-state | copy state into the newly configured backend |
-backend=false | skip backend setup entirely, for a validate-only pass |
-backend-config=FILE or ="k=v" | supply partial backend values |
-lockfile=readonly | do not update .terraform.lock.hcl, error instead |
-plugin-dir=DIR | install providers only from a local mirror |
-json | machine-readable output |
-reconfigure and -migrate-state are not interchangeable. One abandons the association with your old state, the other copies it.
plan and apply
terraform plan terraform plan -out=tfplan # save the plan, then apply exactly it terraform show tfplan # human-readable terraform show -json tfplan | jq '.resource_changes[].change.actions' terraform apply tfplan # no prompt, applies the saved plan terraform apply -auto-approve # skips the prompt, so skips review terraform apply -refresh-only # write detected drift into state terraform apply -replace="aws_instance.web" # force one resource to be replaced terraform apply -parallelism=5 # default is 10 terraform apply -lock-timeout=5m terraform destroy # same as apply -destroy
| Flag | Applies to | Description |
|---|---|---|
-out=FILE | plan | write the plan for a later apply |
-detailed-exitcode | plan | exit code encodes whether there is a diff |
-destroy | plan, apply | plan or run a full destroy |
-refresh-only | plan, apply | reconcile state with reality, propose no changes |
-refresh=false | plan, apply | trust state, skip the provider read |
-replace=ADDR | plan, apply | force-replace one instance |
-target=ADDR | plan, apply | restrict to one address and its dependencies |
-var 'k=v' / -var-file=FILE | plan, apply | set input variables, repeatable |
-parallelism=N | plan, apply | concurrent graph walks, default 10 |
-lock=false / -lock-timeout=D | plan, apply | state locking behavior |
-input=false | all | fail instead of prompting |
-compact-warnings | plan, apply | one line per warning |
-json | plan, apply | streaming machine-readable events |
-generate-config-out=FILE | plan | scaffold HCL for import blocks (1.5+) |
-invoke=ACTION | apply | run one action on its own (1.14+) |
plan -detailed-exitcode is the CI primitive.
| Exit code | Meaning |
|---|---|
0 | success, the diff is empty |
1 | the command errored |
2 | success, and there are changes to apply |
set +e terraform plan -input=false -detailed-exitcode -out=tfplan code=$? set -e case $code in 0) echo "no changes" ;; 2) echo "changes pending, gating apply" ;; *) echo "plan failed"; exit 1 ;; esac
-targetis an escape hatch, not a workflow. It applies a partial graph, so the resulting state does not match your configuration and the next full plan will show surprises. Reach for it to break a dependency deadlock or recover from a failed apply, then run an untargeted plan immediately after. If you find yourself targeting routinely, split the configuration instead.
-auto-approve is correct in a pipeline that already reviewed a saved plan file, and wrong on a laptop. Prefer plan -out then apply tfplan, which also guarantees you apply the diff you read.
Inspecting State and Config
terraform show # current state, human-readable terraform show -json # current state, stable JSON schema terraform state list # every address terraform state list module.vpc # filtered by address prefix terraform state show aws_instance.web terraform state show -json aws_instance.web # 1.16+ terraform state pull > backup.tfstate terraform state push backup.tfstate terraform state replace-provider \ registry.terraform.io/-/aws registry.terraform.io/hashicorp/aws terraform force-unlock LOCK_ID
terraform output # every root output terraform output vpc_id # one, HCL-quoted terraform output -raw db_endpoint # unquoted, for a shell variable terraform output -json | jq -r '.vpc_id.value'
output reads state, not your configuration, so a new output block shows nothing until the next apply.
terraform graph | dot -Tsvg > graph.svg terraform graph -type=plan terraform graph -format=mermaid > graph.mmd # 1.16+ terraform console terraform console -scope=module.vpc # 1.16+, evaluate inside a module echo 'cidrsubnet("10.0.0.0/16", 8, 2)' | terraform console
terraform providers # the provider requirement tree terraform providers schema -json # full schemas, large terraform providers lock \ -platform=linux_amd64 -platform=darwin_arm64 # multi-platform hashes terraform providers mirror ./mirror # populate an offline mirror terraform version # CLI plus provider versions
Checking, Testing and Querying
terraform fmt # rewrite files in this directory terraform fmt -recursive # include subdirectories terraform fmt -check -diff -recursive # exit 1 and show the diff, for CI terraform validate # types, references, required arguments terraform validate -json
validate needs a completed init because it loads provider schemas, but it makes no API calls and needs no credentials. fmt needs neither.
terraform test # runs tests/*.tftest.hcl (1.6+) terraform test -filter=vpc.tftest.hcl terraform test -verbose # show the plan or state per run block terraform test -json terraform test -junit-xml=results.xml # CI test reporting (GA in 1.11) terraform test -test-directory=integration
terraform query # 1.14+ terraform query -generate-config-out=found.tf # scaffold what it found terraform query -json
terraform query evaluates list blocks in .tfquery.hcl files against the provider, which is how you discover existing objects at scale before importing them. It reads nothing from and writes nothing to state.
Deprecated and Replaced
| Old | Use instead |
|---|---|
terraform taint ADDR | terraform apply -replace=ADDR |
terraform untaint ADDR | nothing, -replace needs no marker |
terraform refresh | terraform apply -refresh-only |
terraform state mv A B | a moved block in configuration (1.1+) |
terraform state rm ADDR | a removed block in configuration (1.7+) |
terraform import ADDR ID | an import block plus plan -generate-config-out (1.5+) |
terraform env | terraform workspace |
terraform 0.13upgrade and friends | gone, upgrade one minor version at a time |
The replacements are declarative, so they show up in a plan, get reviewed, and run identically on every machine. See the Import and Refactoring topic.
Environment Variables
| Variable | Effect |
|---|---|
TF_VAR_<name> | set an input variable, parsed as HCL for complex types |
TF_LOG | TRACE, DEBUG, INFO, WARN, ERROR, or JSON |
TF_LOG_CORE / TF_LOG_PROVIDER | log level for just core or just providers |
TF_LOG_PATH | append logs to a file, required for TRACE to be usable |
TF_CLI_ARGS | extra arguments prepended to every command |
TF_CLI_ARGS_<name> | extra arguments for one command, TF_CLI_ARGS_plan |
TF_DATA_DIR | move .terraform, for a read-only checkout |
TF_WORKSPACE | select a workspace without workspace select |
TF_INPUT | 0 behaves like -input=false |
TF_IN_AUTOMATION | any non-empty value trims the "what to do next" hints |
TF_PLUGIN_CACHE_DIR | share downloaded providers across projects |
TF_REGISTRY_DISCOVERY_RETRY | retries for registry discovery, flaky networks |
TF_TOKEN_<host> | API token for a registry or HCP host, _ for dots |
export TF_IN_AUTOMATION=1 export TF_INPUT=0 export TF_PLUGIN_CACHE_DIR="$HOME/.terraform.d/plugin-cache" # Debugging a provider crash TF_LOG=TRACE TF_LOG_PATH=/tmp/tf.log terraform apply
TF_VAR_ values are parsed as HCL, so a list variable needs TF_VAR_zones='["a","b"]'. A plain string needs no quoting.
A CI Sequence That Holds
set -euo pipefail terraform fmt -check -recursive terraform init -input=false -lockfile=readonly terraform validate terraform test set +e terraform plan -input=false -detailed-exitcode -out=tfplan plan_code=$? set -e [ "$plan_code" -eq 1 ] && exit 1 terraform show -json tfplan > plan.json # feed to conftest, checkov, opa # Gated: only on the protected branch, and only with a real diff if [ "$plan_code" -eq 2 ] && [ "$BRANCH" = "main" ]; then terraform apply -input=false tfplan fi
-lockfile=readonlyin CI turns an unreviewed provider upgrade into a failed build instead of a silent one.- Apply the saved plan file, never re-plan inside the apply step. Re-planning applies whatever the world looks like at that moment, which is not what anybody reviewed.
- A plan file may contain sensitive values. Treat it as an artifact with the same access controls as state, and do not attach it to a public build log.