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 = true hides 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 .tfstate file. The only way to keep a value out of state is to make it ephemeral or 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 flagUse when
-migrate-statethe backend block changed and you want the existing state copied over
-reconfigurethe backend block changed and you want to start fresh, discarding the association with the old state
-backend-config=...supplying partial configuration values
-upgradere-resolving provider versions, unrelated to state

-reconfigure and -migrate-state are not interchangeable. -migrate-state moves your state. -reconfigure abandons 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.

BackendLocking
S3use_lockfile = true, or deprecated DynamoDB
GCS, azurerm, HCP Terraform, pg, consul, kubernetesbuilt in
httponly if the server implements the lock endpoints
localnone

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 state subcommand that writes also drops a local .backup file. Control the path with -backup=PATH.
  • With S3 versioning on, a bad write is recoverable from the previous object version.
  • terraform state push overwrites remote state wholesale and checks serial and lineage first. -force skips those checks, which is almost always the wrong call.
  • State has a lineage UUID. Pushing a state with a different lineage means you are mixing two unrelated histories, and Terraform is right to refuse.