Terraform State: Backends, Locking, Remote State, and Drift
Terraform state is the record that maps your configuration to real infrastructure, and a backend is where that record lives. By default Terraform uses the local backend, writing a terraform.tfstate file to your working directory - fine for solo experiments, dangerous for teams. Remote backends such as s3, gcs, azurerm, and kubernetes move state to shared, durable storage and add state locking, which stops two concurrent runs from corrupting the file. This lesson covers exam objective 6 end to end: how the local backend behaves, why locking matters and how to recover a stuck lock, how to configure and migrate remote state with the backend block and partial configuration, how to share outputs between configurations with the terraform_remote_state data source, and how to detect and reconcile drift with -refresh-only plans and the terraform state family of commands. Every command here reflects Terraform 1.12 behavior.
On this page7 sections
- The local backend is Terraform's default
- State locking prevents concurrent writes from corrupting state
- Configuring remote state with the backend block
- Partial configuration and migrating between backends
- Sharing outputs across configurations with terraform_remote_state
- Detecting and reconciling drift
- State surgery: the terraform state commands
- Describe how the default local backend stores state and where it falls short for teams
- Explain how state locking prevents corruption and recover a stuck lock with force-unlock
- Configure remote state with the backend block, including partial configuration at init
- Migrate state safely between backends with terraform init -migrate-state
- Share root module outputs across configurations with the terraform_remote_state data source
- Detect and reconcile drift using -refresh-only and the terraform state subcommands
The local backend is Terraform's default
The local backend is what Terraform uses when your configuration declares no backend or cloud block: it stores state as a terraform.tfstate file in the current working directory and keeps the previous version in terraform.tfstate.backup. You never have to configure it - run terraform init and terraform apply in a fresh directory and the local backend is already in effect.
You can also declare it explicitly, which is mainly useful for pointing state at a non-default path:
terraform {
backend "local" {
path = "state/terraform.tfstate"
}
}The local backend does support state locking - Terraform takes a lock on the state file on disk, so two terminals on the same machine cannot write it simultaneously. What it cannot do is share state: the file exists only on one machine, teammates cannot see it, there is no central lock across machines, and any sensitive attribute values (passwords, keys) sit in plain text on a laptop. That combination is exactly why the exam expects you to reach for a remote backend for any team or production workflow.
State locking prevents concurrent writes from corrupting state
State locking makes Terraform acquire a lock before any operation that could write state - plan, apply, destroy, and the state-modifying subcommands - and hold it until the operation finishes. Without it, two people running terraform apply against the same state at the same time could interleave writes and leave the file corrupted or missing resources, which is one of the worst failure modes in Terraform. Locking happens automatically whenever the backend supports it; you do not enable it in configuration.
Most commonly used backends support locking: local (a lock on the state file), s3 (a native S3 lock file via use_lockfile = true; the older DynamoDB-table mechanism is deprecated), gcs, azurerm (a blob lease), kubernetes, consul, pg, and the remote backend / HCP Terraform. If another run holds the lock, your command fails with a lock error that includes a lock ID.
Two flags control locking behavior per command, and one command exists for emergencies:
terraform apply -lock-timeout=5m # retry acquiring the lock for up to 5 minutes terraform apply -lock=false # skip locking entirely (discouraged) terraform force-unlock LOCK_ID # manually release a stuck lock
terraform force-unlock is a last resort: use it only when a lock was left behind by a crashed run and you are certain no other operation is still in progress. It takes the lock ID reported in the error and only releases locks for the current configuration. Disabling locking with -lock=false reintroduces the corruption risk locking exists to prevent, so the exam treats it as almost never the right answer.
Configuring remote state with the backend block
You configure remote state by adding a backend block inside the top-level terraform block, naming a backend type and its settings. A configuration can have at most one backend (or a cloud block for HCP Terraform, but not both). Terraform 1.12 ships backends including s3, gcs, azurerm, kubernetes, consul, pg, http, oss, cos, and remote.
terraform {
backend "s3" {
bucket = "acme-tf-state-prod"
key = "network/terraform.tfstate"
region = "us-east-1"
use_lockfile = true
}
}A Google Cloud equivalent uses the gcs backend, which locks natively with no extra settings:
terraform {
backend "gcs" {
bucket = "acme-tf-state"
prefix = "prod/network"
}
}Two rules are heavily tested. First, backend configuration is evaluated before almost anything else, so it cannot reference variables, locals, or any named values - every argument must be a literal (or be supplied at init time, covered next). Second, any change to the backend block only takes effect after you re-run terraform init. Remote state gives the team one shared source of truth with locking and durable storage, but remember the state itself still contains sensitive values - protect the bucket or container with encryption and tight access controls.
Partial configuration and migrating between backends
Partial configuration means leaving some backend arguments out of the backend block and supplying them when you run terraform init. Because the block cannot use variables, this is the supported way to keep secrets and per-environment values (bucket names, access credentials) out of version control. You pass the missing values with -backend-config, either as a file of key/value pairs or as individual pairs:
terraform init -backend-config=prod.s3.tfbackend terraform init \ -backend-config="bucket=acme-tf-state-prod" \ -backend-config="key=network/terraform.tfstate" \ -backend-config="region=us-east-1"
Terraform merges these with whatever is in the block and can prompt interactively for anything still missing.
When you change the backend - say, moving from local to s3 - terraform init detects the change and refuses to proceed until you choose what to do with existing state. Run terraform init -migrate-state to copy the current state into the new backend (Terraform prompts for confirmation), or terraform init -reconfigure to adopt the new backend configuration while ignoring the old one entirely, without migrating anything. Migration is the normal path: it is how you promote a laptop-only project to shared remote state without losing the mapping to existing infrastructure.
Detecting and reconciling drift
Drift is when real infrastructure no longer matches what Terraform state recorded - someone resized an instance in the console, a security rule was deleted by hand, an external process changed a tag. Terraform detects drift by refreshing: reading the real remote objects through the provider and comparing them to state. A normal terraform plan refreshes first, then diffs the refreshed picture against your configuration, so drifted attributes show up as planned changes; since Terraform 0.15.4 the refreshed data is used in memory for that plan but is not persisted to the state file by plan itself.
To look at drift in isolation, use refresh-only mode. A refresh-only plan proposes no infrastructure changes at all - it only shows what Terraform would update in state to match reality, and a refresh-only apply writes those updates to state:
terraform plan -refresh-only # preview drift: what state would change terraform apply -refresh-only # accept drift into state, change no infrastructure
The standalone terraform refresh command still exists but is deprecated: it behaves like terraform apply -refresh-only -auto-approve, silently rewriting state with no chance to review, which is exactly why the reviewed -refresh-only workflow replaced it. Once drift is visible you have two honest options: run a normal terraform apply to push infrastructure back to what the configuration declares, or update the configuration (and accept the drift with a refresh-only apply) if the out-of-band change is what you actually want.
State surgery: the terraform state commands
The terraform state subcommands (plus a few related top-level commands) let you inspect and rewrite the state's mapping without touching infrastructure. These appear constantly in objective 6 questions:
| Command | What it does |
|---|---|
terraform state list | Lists the addresses of all resources tracked in state |
terraform state show ADDRESS | Prints every recorded attribute of one resource |
terraform state mv SOURCE DEST | Moves/renames a resource address in state without destroying it |
terraform state rm ADDRESS | Removes a resource from state; the real object survives, unmanaged |
terraform state pull | Downloads the raw state and writes it to stdout |
terraform state push | Uploads a local state file to the backend (dangerous; rarely needed) |
terraform taint ADDRESS | Deprecated: marks a resource for recreation; use -replace instead |
terraform apply -replace=ADDRESS | Plans and applies the destroy-and-recreate of one resource |
terraform force-unlock LOCK_ID | Releases a stuck state lock (last resort) |
The deprecation pattern is a favorite exam theme: terraform taint gave way to the plannable terraform apply -replace="aws_instance.web", just as terraform refresh gave way to -refresh-only. Similarly, modern configuration-driven alternatives exist for state surgery itself - a moved block records a rename in configuration instead of a one-off state mv, and a removed block (Terraform 1.7+) can forget a resource in a reviewed plan instead of state rm.
terraform state mv aws_instance.app aws_instance.web terraform apply -replace="aws_instance.web"
Remember the boundary: state mv and state rm edit only Terraform's records. state rm never destroys anything - the resource keeps running, Terraform just stops managing it (and a later plan may propose creating a duplicate if the configuration still declares it).
Tip. Expect scenario questions on which backends support state locking, what -lock-timeout and force-unlock do, and why -lock=false is discouraged. You will be tested on backend block rules (no variables, one backend, re-init after changes), partial configuration with -backend-config, and choosing -migrate-state versus -reconfigure. Drift questions hinge on the difference between terraform plan -refresh-only, terraform apply -refresh-only, and the deprecated terraform refresh, plus knowing that state rm forgets without destroying and that taint is superseded by -replace.
- The local backend is the default: state lives in terraform.tfstate in the working directory, with terraform.tfstate.backup as the previous version - workable solo, unshareable for teams.
- State locking is automatic on supporting backends (local, s3, gcs, azurerm, kubernetes, consul, pg, remote); -lock-timeout waits for a lock, -lock=false skips it, and force-unlock LOCK_ID is strictly a last resort.
- The backend block lives inside the terraform block, allows one backend per configuration, and cannot reference variables - every value is a literal or supplied at init.
- Partial configuration with terraform init -backend-config (a file or key=value pairs) keeps secrets and per-environment settings out of version control.
- Changing backends requires re-running terraform init: -migrate-state copies existing state to the new backend, -reconfigure adopts the new config without migrating.
- terraform_remote_state exposes only the producing configuration's root module outputs, and the consumer needs read access to the whole state file.
- Drift is detected by refreshing: terraform plan -refresh-only previews it, terraform apply -refresh-only accepts it into state, and the old terraform refresh command is deprecated.
- terraform taint is deprecated in favor of terraform apply -replace=ADDRESS; state rm forgets a resource without destroying it.
Frequently asked questions
What backend does Terraform use if I never configure one?
The local backend. Terraform writes state to a terraform.tfstate file in the working directory and keeps the prior version in terraform.tfstate.backup. It supports locking on that one machine, but the state cannot be shared with teammates, which is why remote backends are recommended for any collaborative work.
Can I use variables inside a backend block?
No. Backend configuration is processed during terraform init, before variables are evaluated, so every argument must be a literal value. To vary settings per environment or keep secrets out of version control, use partial configuration and pass the missing arguments at init time with -backend-config, either as key=value pairs or a config file.
When is it safe to run terraform force-unlock?
Only when a lock is genuinely stuck - typically because a Terraform process crashed or lost connectivity before releasing it - and you have confirmed no run is still in progress. You pass the lock ID from the error message, and Terraform only releases locks belonging to the current configuration. If another apply really is running, force-unlocking reopens the door to state corruption.
What is the difference between terraform refresh and terraform plan -refresh-only?
terraform refresh is deprecated: it immediately rewrites state to match real infrastructure with no review step, equivalent to terraform apply -refresh-only -auto-approve. The modern workflow is terraform plan -refresh-only to preview exactly what would change in state, then terraform apply -refresh-only to accept it. Neither mode ever changes real infrastructure.
Does terraform_remote_state let me read another configuration's resources directly?
No. It exposes only the values the other configuration declared as outputs in its root module. Resources, and outputs of nested modules that were not re-exported from the root, are not reachable. The consumer also needs permission to read the entire remote state file, so provider-native data sources are often a better-scoped way to look up shared infrastructure.
Does terraform state rm destroy the resource?
No. It only removes the resource from Terraform's state, so Terraform stops tracking it while the real object keeps running. If the resource is still declared in configuration, the next plan will propose creating a new one. To actually destroy a resource, remove it from configuration and apply, or use terraform destroy with targeting.
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.