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

CommandWhat it does
initdownload providers and modules, configure the backend
validatecheck syntax, types and internal consistency, no remote calls
fmtrewrite files to canonical style
plancompute and show the diff
applymake the diff real
destroyshorthand for apply -destroy
testrun .tftest.hcl test files (1.6+)
queryrun .tfquery.hcl List Resource queries (1.14+)
showrender state or a saved plan file
outputprint root module output values
stateinspect and surgically edit state
consoleinteractive expression evaluator
graphemit the dependency graph
providersinspect provider requirements, schemas and the lock file
getdownload modules only
workspacemanage named state files
force-unlockrelease a stuck state lock
importwrite an existing object into state
login / logoutcredentials for HCP Terraform and private registries
versionversion 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
FlagDescription
-upgradere-resolve provider and module versions within the constraints
-reconfigureignore the existing backend record and start fresh
-migrate-statecopy state into the newly configured backend
-backend=falseskip backend setup entirely, for a validate-only pass
-backend-config=FILE or ="k=v"supply partial backend values
-lockfile=readonlydo not update .terraform.lock.hcl, error instead
-plugin-dir=DIRinstall providers only from a local mirror
-jsonmachine-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
FlagApplies toDescription
-out=FILEplanwrite the plan for a later apply
-detailed-exitcodeplanexit code encodes whether there is a diff
-destroyplan, applyplan or run a full destroy
-refresh-onlyplan, applyreconcile state with reality, propose no changes
-refresh=falseplan, applytrust state, skip the provider read
-replace=ADDRplan, applyforce-replace one instance
-target=ADDRplan, applyrestrict to one address and its dependencies
-var 'k=v' / -var-file=FILEplan, applyset input variables, repeatable
-parallelism=Nplan, applyconcurrent graph walks, default 10
-lock=false / -lock-timeout=Dplan, applystate locking behavior
-input=falseallfail instead of prompting
-compact-warningsplan, applyone line per warning
-jsonplan, applystreaming machine-readable events
-generate-config-out=FILEplanscaffold HCL for import blocks (1.5+)
-invoke=ACTIONapplyrun one action on its own (1.14+)

plan -detailed-exitcode is the CI primitive.

Exit codeMeaning
0success, the diff is empty
1the command errored
2success, 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

-target is 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

OldUse instead
terraform taint ADDRterraform apply -replace=ADDR
terraform untaint ADDRnothing, -replace needs no marker
terraform refreshterraform apply -refresh-only
terraform state mv A Ba moved block in configuration (1.1+)
terraform state rm ADDRa removed block in configuration (1.7+)
terraform import ADDR IDan import block plus plan -generate-config-out (1.5+)
terraform envterraform workspace
terraform 0.13upgrade and friendsgone, 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

VariableEffect
TF_VAR_<name>set an input variable, parsed as HCL for complex types
TF_LOGTRACE, DEBUG, INFO, WARN, ERROR, or JSON
TF_LOG_CORE / TF_LOG_PROVIDERlog level for just core or just providers
TF_LOG_PATHappend logs to a file, required for TRACE to be usable
TF_CLI_ARGSextra arguments prepended to every command
TF_CLI_ARGS_<name>extra arguments for one command, TF_CLI_ARGS_plan
TF_DATA_DIRmove .terraform, for a read-only checkout
TF_WORKSPACEselect a workspace without workspace select
TF_INPUT0 behaves like -input=false
TF_IN_AUTOMATIONany non-empty value trims the "what to do next" hints
TF_PLUGIN_CACHE_DIRshare downloaded providers across projects
TF_REGISTRY_DISCOVERY_RETRYretries 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=readonly in 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.