Terraform Cheatsheet
State
Use this Terraform reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
What State Is
Terraform records every object it manages in a JSON state file. State maps your config addresses to real resource IDs and caches the last-known attribute values, dependency edges and provider metadata. Without it Terraform cannot tell "create this" from "update this".
terraform show # human-readable current state terraform show -json # machine-readable, stable schema terraform state list # just the addresses terraform state show aws_instance.web # one resource (-json in 1.16+)
Never hand-edit state. Use moved / removed blocks (see Import and Refactoring) or, as a last resort, a terraform state subcommand after taking a backup.
State contains secrets in plaintext. Database passwords, private keys, generated tokens, and every attribute a provider marked sensitive are all written to state unencrypted.
sensitive = truehides values from the terminal, nothing more. Treat the state file exactly as you would treat the secrets inside it: encrypt the bucket at rest, restrict access to the smallest possible set of principals, enable versioning so a bad write is recoverable, turn on access logging, and never commit a.tfstatefile. The only way to keep a value out of state is to make itephemeralor pass it through a write-only argument.
Backends
A backend decides where state lives and how it is locked. Only the root module may declare one, and the block takes literal values only, no variables or functions.
S3
terraform { backend "s3" { bucket = "acme-tf-state" key = "prod/us-east-1/terraform.tfstate" region = "us-east-1" encrypt = true kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/abcd-1234" use_lockfile = true # S3-native locking, GA in 1.11 } }
use_lockfile = true writes a <key>.tflock object next to the state and relies on S3 conditional writes. DynamoDB-based locking is deprecated and will be removed in a future minor version, so dynamodb_table should go away.
# Migration window only: both mechanisms at once, so older CLIs # on the same state keep working. Then delete dynamodb_table and # the table itself. backend "s3" { bucket = "acme-tf-state" key = "prod/terraform.tfstate" region = "us-east-1" use_lockfile = true dynamodb_table = "tf-state-lock" # deprecated }
Enable S3 bucket versioning. It is the only realistic recovery path from a corrupted or truncated state write.
GCS, azurerm, and other backends
backend "gcs" { bucket = "acme-tf-state" prefix = "terraform/prod" # not "key", GCS uses a prefix } backend "azurerm" { resource_group_name = "tf-state-rg" storage_account_name = "acmetfstate" container_name = "tfstate" key = "prod.terraform.tfstate" use_azuread_auth = true }
Also available: kubernetes, consul, http, pg, oss, cos, oci (1.12+), remote, and local. GCS and azurerm lock natively with no extra setup.
HCP Terraform
terraform { cloud { organization = "acme" workspaces { name = "prod" # or: tags = ["app:web", "env:prod"] for many workspaces } } }
cloud replaces backend, not adds to it. HCP Terraform stores state, versions it, locks it, and can run plans and applies remotely. There is no bucket to secure yourself.
Local (default)
backend "local" { path = "terraform.tfstate" }
No locking and no sharing, so two people applying at once silently corrupt each other's work. Fine for a scratch directory, never for a team.
Partial Configuration and Migrating Backends
Backend blocks cannot use variables, so per-environment values come in through partial configuration at init time.
# versions.tf: omit the values that vary terraform { backend "s3" { encrypt = true use_lockfile = true } }
# backends/prod.s3.hcl bucket = "acme-tf-state" key = "prod/terraform.tfstate" region = "us-east-1"
terraform init -backend-config=backends/prod.s3.hcl terraform init -backend-config="key=staging/terraform.tfstate" # single value terraform init -backend=false # skip the backend entirely (validate only)
| Init flag | Use when |
|---|---|
-migrate-state | the backend block changed and you want the existing state copied over |
-reconfigure | the backend block changed and you want to start fresh, discarding the association with the old state |
-backend-config=... | supplying partial configuration values |
-upgrade | re-resolving provider versions, unrelated to state |
-reconfigureand-migrate-stateare not interchangeable.-migrate-statemoves your state.-reconfigureabandons the link to it, and if you have not backed up you may have just orphaned a whole environment. When Terraform asks "Do you want to copy existing state to the new backend?", read the question.
Because -backend-config values are baked into .terraform/terraform.tfstate (the backend record, not your real state), switching environments in the same directory needs a re-init. Most teams use a directory per environment instead.
Locking
Terraform takes a write lock for plan, apply, destroy and any state-writing command, so two runs cannot interleave.
terraform apply -lock-timeout=5m # wait instead of failing immediately terraform plan -lock=false # skip locking, dangerous, read-only cases terraform force-unlock LOCK_ID # release a lock left by a crashed run terraform force-unlock -force LOCK_ID # no confirmation prompt
The lock ID appears in the error message from the blocked run. Before you force-unlock, confirm no run is actually live. Breaking a lock during a real apply is how state gets corrupted.
| Backend | Locking |
|---|---|
| S3 | use_lockfile = true, or deprecated DynamoDB |
| GCS, azurerm, HCP Terraform, pg, consul, kubernetes | built in |
| http | only if the server implements the lock endpoints |
| local | none |
Workspaces and State Layout
Each workspace gets its own state file behind the same backend config.
s3://acme-tf-state/ ├── prod/terraform.tfstate # the "default" workspace, your key └── env:/ # workspace_key_prefix, default "env:" ├── staging/prod/terraform.tfstate └── dev/prod/terraform.tfstate
The default workspace uses key verbatim. Every other workspace is stored at <workspace_key_prefix>/<name>/<key>. See the Workspaces topic for when this is and is not the right isolation model.
Reading Another State's Outputs
data "terraform_remote_state" "vpc" { backend = "s3" config = { bucket = "acme-tf-state" key = "shared/vpc/terraform.tfstate" region = "us-east-1" } } resource "aws_instance" "app" { subnet_id = data.terraform_remote_state.vpc.outputs.public_subnet_ids[0] }
This reads the whole state file, so the caller needs read access to everything in it, secrets included. Where you can, publish the handful of values other stacks need (SSM Parameter Store, HCP Terraform tfe_outputs) and read those instead.
Backup and Recovery
# Always, before any state surgery terraform state pull > backup-$(date +%Y%m%d-%H%M).tfstate # Restore terraform state push backup-20260915-1400.tfstate
- Every
terraform statesubcommand that writes also drops a local.backupfile. Control the path with-backup=PATH. - With S3 versioning on, a bad write is recoverable from the previous object version.
terraform state pushoverwrites remote state wholesale and checks serial and lineage first.-forceskips those checks, which is almost always the wrong call.- State has a
lineageUUID. Pushing a state with a different lineage means you are mixing two unrelated histories, and Terraform is right to refuse.