# Terraform CLI Cheat Sheet — State & Workspaces

> **Tool:** Terraform
> **Category:** Infrastructure as Code
> **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:** https://developer.hashicorp.com/terraform/cli

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

```mermaid
sequenceDiagram
    participant You as You (terraform apply)
    participant Backend as Remote Backend (S3+DynamoDB, etc.)
    participant Teammate as A Teammate

    You->>Backend: Acquire state lock
    Teammate->>Backend: Also tries terraform apply
    Backend-->>Teammate: "Error: state locked" — blocked, not silently allowed to race
    You->>Backend: Apply, write new state
    You->>Backend: Release lock
    Backend-->>Teammate: Lock now free — safe to proceed
```

> [!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

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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)

```bash
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

```bash
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

```bash
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:

```bash
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-unlock`ing 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.
