Verified10 commandsAI-assisted

State & Workspaces

.md

Verified against Terraform v1.9.8, flags verified via `terraform <cmd> -help` run locally; taint-vs-replace guidance cross-checked against developer.hashicorp.com/terraform/cli docs (not executed against real infra), 2026-08-29 · official docs

Inspecting and surgically editing state, managing workspaces, importing existing infrastructure, and reading outputs. ⚙️

Remote state and locking — why this matters before touching any state command#

Diagram

Warning

Every command on this page assumes a properly configured remote backend with locking (S3+DynamoDB, Terraform Cloud, GCS, etc.) — local state (the default with no backend block) has no locking at all and is genuinely dangerous for anything beyond solo experimentation: two people running apply against the same local state file concurrently can corrupt it, with no built-in protection. Configuring a real remote backend is the first thing any team Terraform setup should do, before any of state mv/state rm/import.

Inspecting state#

terraform state list                          # every resource instance in state
terraform state list module.vpc               # filter to one module
terraform state show aws_instance.web          # full attributes of one resource, as currently in state

Moving and removing state entries#

terraform state mv aws_instance.old aws_instance.new    # rename a resource without destroy/recreate
terraform state mv module.old_name module.new_name       # move an entire module
terraform state rm aws_instance.web                        # forget a resource without destroying it in real infra

state rm does not touch the real infrastructure — it only makes Terraform stop tracking the resource. Useful when a resource should now be managed elsewhere (a different state file, or manually), but never a way to "delete" something you actually want gone.

Workspaces#

terraform workspace list
terraform workspace new staging
terraform workspace select staging
terraform workspace select production -or-create   # switch, creating it first if it doesn't exist
terraform workspace show                            # print the currently-selected workspace's name
terraform workspace delete staging                   # delete a workspace (fails if it's managing resources)
terraform workspace delete -force staging             # delete even if Terraform still thinks it manages resources

Workspaces give you multiple independent state files from one configuration — useful for environment isolation (dev/staging/prod) with the same code, but each workspace still shares the same backend config and provider credentials. For genuinely separate environments with different credentials/accounts, separate root modules (not workspaces) are the more common enterprise pattern.

${terraform.workspace} is available inside your .tf config to key off the current workspace name — e.g. count = terraform.workspace == "prod" ? 3 : 1 — but this couples your logic to workspace naming, which is easy to typo and has no built-in validation. For anything beyond a small dev/staging split, an explicit var.environment passed via -var-file is usually easier to reason about and review in a plan.

workspace delete -force unregisters the workspace's state from Terraform's bookkeeping — it does not destroy the real infrastructure that workspace was managing. Always terraform destroy in that workspace first if the infrastructure itself should also go away.

Importing existing infrastructure#

terraform import aws_instance.web i-0123456789abcdef0

import only populates state — it does not generate the matching .tf configuration for you. You still have to hand-write a resource block that matches the imported object's real attributes, or the next plan will show a large diff trying to reconcile your (empty/wrong) config against the imported state.

Reading outputs#

terraform output                              # all outputs
terraform output instance_ip                   # a single output's value
terraform output -json                          # machine-readable, for piping into another tool/script
terraform output -raw instance_ip                # raw string, no quotes — for shell scripting

Forcing resource replacement#

terraform apply -replace=aws_instance.web       # current recommended way to force a resource to be destroyed + recreated
terraform taint aws_instance.web                # older command, still works — marks a resource tainted for the next plan
terraform untaint aws_instance.web

-replace on plan/apply is the currently-documented approach (per official docs) for forcing replacement — taint/untaint still function in this version but are the older mechanism; prefer -replace in new scripts and runbooks.

Interactive console (for testing expressions)#

terraform console

Opens a REPL that loads current state and lets you evaluate expressions/interpolations (e.g. aws_instance.web.public_ip) before committing them to a config file — read-only, never modifies state.

Inspecting resolved provider requirements#

terraform providers                       # tree of modules annotated with their provider requirements
terraform providers lock                  # write/refresh .terraform.lock.hcl for the constrained providers
terraform providers schema -json          # full JSON schema for every provider used in the config

providers schema -json is mostly a building block for tooling (linters, policy checks, docs generators) rather than something you read directly — it dumps the complete resource/data-source attribute schema for every provider currently in use.

Recovering from a force-unlocked state#

terraform force-unlock <lock-id>          # manually release a stuck lock, e.g. after a crashed CI job

Caution

Only run force-unlock after confirming no other Terraform process is actually still running against that state. A stuck lock is usually a genuine crash artifact (a CI job killed mid-apply), but force- unlocking while an apply is still legitimately in progress elsewhere reopens exactly the concurrent-write corruption risk locking exists to prevent. Check with the team / CI history before running this, not just because the lock is inconvenient.

Real-world scenario: safely renaming a resource without downtime#

A team renames aws_instance.web to aws_instance.app_server in their config for clarity — without state surgery, Terraform would see this as "destroy the old, create a new one," which for many resource types means real downtime and, for something like a database, potential data loss:

terraform state mv aws_instance.web aws_instance.app_server
terraform plan     # confirm: no changes — this proves the rename didn't trigger a destroy/recreate

Tip

Always run terraform plan immediately after any state mv/state rm to confirm the diff is now empty (or exactly what you expected). state mv succeeding doesn't guarantee the rename was interpreted correctly — if the resource's for_each/count addressing changed shape too, the plan can still show an unexpected destroy/recreate even after the move, and it's much cheaper to catch that in a plan than to discover it mid-apply.

Common pitfalls#

  • Running any state command against a local, unlocked backend on a team project — see the WARNING above.
  • force-unlocking a lock without confirming nothing else is actually running — see the CAUTION above.
  • Skipping the post-state mv plan check — see the TIP above; a "successful" state move isn't proof the rename didn't still trigger an unwanted destroy/recreate.
  • Using terraform.workspace for anything beyond a small dev/staging split — see the earlier note on workspaces; an explicit var.environment is more reviewable and typo-resistant at real scale.

When to reach for something else#

For genuinely separate environments with different cloud accounts/credentials (not just different variable values), separate root modules — not workspaces — are the standard enterprise pattern; workspaces share backend config and credentials, which is the wrong isolation boundary once "dev" and "prod" mean different AWS accounts, not just different variable files.