SaveMyCert
Log in
5 of 5 free questions left today·for 10 a day
Terraform state and backend operations

Terraform Professional: Inspecting State and Remediating Drift

16 min readTerraform Pro 002 · Terraform state and backend operationsUpdated

Inspecting Terraform state means reading what Terraform has recorded about your infrastructure with commands such as terraform state list, state show, show and console, and remediating drift means bringing state, configuration and real objects back into agreement after something changed outside Terraform. This lesson covers both halves of the Professional exam's state objectives: the read-only commands and their sensitive-value pitfalls, backing up and restoring state, accepting or reverting drift with refresh-only and normal runs, the state surgery tools (removed and moved blocks, state rm, state mv, -replace, untaint, replace-provider), HCP Terraform health assessments, and the lifecycle and provider settings that tolerate drift another system is meant to cause.

What you’ll learn
  • Choose between state list, state show, show, output, console and state pull, and build correctly quoted instance addresses for modules and for_each keys
  • Explain which commands print sensitive values in plain text and how to filter them with sensitive_values
  • Back up remote state with state pull and explain the lineage and serial checks on state push and what -force does
  • Accept drift with refresh-only plus a configuration change, revert it with a normal apply, and recognise settings that hide it
  • Forget, re-address or force replacement of objects with removed and moved blocks, state rm, state mv, -replace and untaint
  • Configure, troubleshoot and act on HCP Terraform health assessments
  • Tolerate expected drift with ignore_changes and the AWS provider's ignore_tags, and repoint state with state replace-provider

Which commands inspect Terraform state without changing it?

Five commands read state and never write a new snapshot or touch infrastructure: terraform state list, terraform state show, terraform show, terraform output and terraform state pull. Each answers a different question, and picking the wrong one usually fails because it takes the wrong kind of argument.

CommandArgument it takesWhat it prints
terraform state list [ADDRESS...]Optional address filters (for example module.network), or -id=IDResource instance addresses only, no attributes
terraform state show ADDRESSExactly one resource instance addressEvery attribute recorded for that instance
terraform show [PATH]Optional path to a saved plan or state file (never an address)The whole latest state snapshot (or the file); -json for machine-readable output
terraform output [NAME]Optional root output nameRoot module output values only
terraform state pullNoneThe raw state snapshot from the backend, to stdout

terraform state list is the inventory tool. A prefix such as module.network filters to every instance inside that module, and -id=vol-0f3c... filters to instances whose id attribute matches, which is how you turn a cloud ID from an alert into a Terraform address. state show cannot search: it needs the address already. terraform output never sees resource attributes unless a root output exposes them.

By contrast, terraform refresh, terraform state push, state rm, state mv, state replace-provider, taint and untaint all write a new state snapshot. During a change freeze that distinction is the whole rule.

What does terraform console do, and why can it block a pipeline?

terraform console opens an interactive prompt that evaluates any expression, including functions, against the current workspace's state: length(aws_lb_target_group.api), aws_route_table.private["az1"].id, cidrsubnet(var.cidr, 8, 2). It creates no plan and changes nothing, which makes it the right tool when you need to compute from state rather than just print it.

The catch is locking. With a remote backend, console reads state from the backend and holds a state lock for as long as the session is open. A console left running in a terminal therefore makes every state-writing operation on that workspace (a pipeline apply, for example) fail with "Error acquiring the state lock".

When the lock holder is known and still alive, release the lock from its owner: exit the console. terraform force-unlock LOCK_ID is for locks whose owner has died; using it against a live process overrides protection that process still relies on. -lock=false disables locking entirely, and -lock-timeout only waits for a lock that will never be released on its own.

How do you build a resource instance address for state show?

A resource instance address is built segment by segment: the module path, then type.name, then an instance key on whichever block repeats. count keys are numbers ([0]); for_each keys are quoted strings (["green"]).

ConfigurationAddress of one instance
Resource with for_each inside module cachemodule.cache.aws_elasticache_cluster.node["blue"]
Module call site with for_each; single resource insidemodule.site["tokyo"].aws_route53_zone.this
Resource with count in the root moduleaws_nat_gateway.egress[1]

The key attaches to the segment that carries count or for_each. If the module call repeats and the resource inside does not, the key goes on module.site, never on the resource. Dropping the module path, using a numeric index for a for_each resource, or putting the key on the wrong segment produces an address that matches nothing.

Shell quoting matters. On Linux and macOS shells, wrap the whole address in single quotes so the inner double quotes reach Terraform intact:

terraform state show 'module.cache.aws_elasticache_cluster.node["blue"]'

Without the single quotes, bash strips the double quotes and Terraform receives an invalid key. Windows cmd.exe and PowerShell need different escaping; HashiCorp's state command pages show each form.

Why do sensitive values leak from show -json and output -json?

Sensitive values leak because Terraform stores them in state in plain text and only its human-readable output redacts them. Marking a variable, output or provider attribute sensitive hides it in plan, apply, plain terraform show and the plain terraform output listing, where it appears as (sensitive value) or <sensitive>. Machine-readable forms print the real value:

  • terraform show -json prints every attribute in state, sensitive ones included, in plain text. The JSON carries a sensitive_values structure per resource that marks which attributes are sensitive, so a pipeline that must not log secrets can strip exactly those paths. Filtering for the string "(sensitive value)" removes nothing, because that placeholder never appears in JSON.
  • terraform output -json and terraform output -raw NAME print sensitive root outputs in plain text. -raw exists so scripts can consume a single string value.

The consequence: anyone who can read state can read every secret in it. Protect the state store and its access policy, and treat show -json, output -json and output -raw in shared CI logs as exposure points. (Choosing a secrets strategy so that fewer secrets land in state belongs to the security domain.)

How do state pull and state push protect a backup and restore?

terraform state pull downloads the raw current snapshot from the configured backend and prints it to stdout, so terraform state pull > pre-refactor-backup.tfstate is the standard backup before risky state work. It changes nothing. The output is the real state format, with its lineage (a unique ID assigned when the state was first created) and serial (a counter that increases with every write), so it can be pushed back unchanged. terraform show -json is a different, read-only JSON representation and is not a restorable snapshot.

terraform state push FILE overwrites the remote snapshot and has two safety checks. It refuses when:

  • the lineage differs: the file probably belongs to a different state altogether;
  • the destination serial is higher than the file's: the remote state holds writes the file does not.

A snapshot pulled from the same workspace yesterday shares its lineage, so restoring it after newer writes trips the serial check. -force disables both checks and the remote snapshot is replaced entirely; nothing is merged. Before forcing, confirm that every change recorded after the file's serial is meant to be thrown away.

What is drift, and how do you accept or revert it?

Drift is a difference between the real infrastructure and what Terraform recorded or declared, caused by changes made outside Terraform (a console edit, an autoscaler, another tool). You either revert it to the configuration or accept it as the new desired state.

Refresh-only mode is how you accept drift into state. terraform plan -refresh-only reads the real objects and shows how state would change to match them, proposing no infrastructure actions; it writes nothing. terraform apply -refresh-only shows the same and, once approved, writes the updated state. HashiCorp recommends it over the older terraform refresh, which applies the same update without a review step.

A refresh-only apply fixes state only. The configuration is still the desired state, so:

  • if an operator grew an EBS volume from 50 to 80 GiB and the configuration still says 50, the next normal apply plans an in-place change back to 50 (which fails at the API, since EBS volumes cannot shrink). To keep 80, also change the configuration;
  • if an object was deleted outside Terraform, the refresh-only apply drops it from state, and the next normal plan proposes creating it again while its block remains. To accept the deletion, remove the block too.

The refresh-only plan is also the complete drift report. A normal plan refreshes too, but its "Objects have changed outside of Terraform" note lists only external changes that may have affected the actions it proposes; a refresh-only plan lists every difference between state and reality.

How do you revert drift, and what hides it?

To revert drift, run a normal terraform plan and apply. The plan refreshes state from the real objects, compares them with the configuration and proposes whatever brings reality back: an in-place update for a changed attribute, or a create for an object that was deleted outside Terraform. No import or state edit is needed first; import is for adopting objects that exist but are untracked.

Drift only becomes visible when Terraform reads the real objects. Anything that skips that read cannot detect it:

The rule: drift is found only by a command that refreshes, meaning it asks the providers for the real objects. Flags that shape output, concurrency or locking leave the refresh in place.

GroupExamplesSees drift?
Skips the refreshplan -refresh=falseNo: plans against stored state, so a console change looks like "no changes"
Reads only the stored snapshotstate list, state show, show (plain or -json), output, state pullNo: none of them contacts a provider
Refreshes, then plansplan, plan -refresh-only, plan -detailed-exitcode (exit 0 no changes, 1 error, 2 changes present)Yes
Changes how the run behaves, not what it reads-parallelism=N, -lock=false, -lock-timeout, -out=FILE, -input=false, -no-colorYes, as long as the refresh is still on

-refresh=false trades correctness for speed on large workspaces; HashiCorp warns it can produce an incomplete or incorrect plan. Never use it in a job whose purpose is finding drift.

How do you stop managing an object without destroying it?

To forget an object while it keeps running, remove it from state with either a removed block (reviewed, in a plan) or terraform state rm (immediate CLI edit). Deleting the resource block on its own is not enough: Terraform then plans to destroy the object.

removed {
  from = aws_db_instance.reporting

  lifecycle {
    destroy = false
  }
}
removed blockterraform state rm
Where the change happensIn a normal plan/apply, visible in reviewImmediately, outside any plan
Default effectdestroy defaults to true, so set destroy = false to forget onlyAlways forgets only; never destroys
Resource blockDelete it (the removed block replaces it)You must delete it yourself
Single instance keyNot allowed: from cannot include an instance keyAccepts instance addresses, including keys

HashiCorp recommends removed blocks because the change shows in the plan. Two traps:

  • state rm with the block left in place. The next plan sees a declared resource with nothing tracked and proposes to create it; with a fixed unique name (an S3 bucket, say), the apply then fails because the surviving object holds the name.
  • One instance of a for_each set. A removed block cannot target aws_iam_user.contractor["temp-ops"], and pointing it at aws_iam_user.contractor forgets every instance. Drop the key from the for_each collection and run terraform state rm 'aws_iam_user.contractor["temp-ops"]'.

How do you change an object's address or provider in state?

When a resource is renamed, moved into a module or re-keyed, its address changes; unless state is told, Terraform plans to destroy the object at the old address and create a new one at the new address. A moved block or terraform state mv rebinds the existing object instead.

moved {
  from = aws_sqs_queue.jobs
  to   = aws_sqs_queue.tasks
}
  • Direction. from is the old address that state currently tracks; to is the new address in configuration. Reversed, the block points at an address with nothing tracked.
  • moved block vs state mv. A moved block is applied during a normal plan, so the move appears in review and works for every workspace that uses the code. terraform state mv SOURCE DESTINATION edits state immediately; add -dry-run first to list the instances that would match without changing anything. Moving a block into a child module observability is terraform state mv aws_cloudwatch_log_group.audit module.observability.aws_cloudwatch_log_group.audit.
  • count to for_each. aws_sqs_queue.dlq[0] and aws_sqs_queue.dlq["orders"] are unrelated addresses, and Terraform does not map indexes to keys in any order. Write one moved block per instance. The only moves Terraform infers on its own are between an unkeyed address and index [0] when count is added or removed.

An import block is not a substitute: it adopts an untracked object at the new address but leaves the old address in state, planned for destruction of that same object. Import workflows themselves belong to the infrastructure-lifecycle domain.

How do you repoint state to a new provider source?

Every resource in state records the fully qualified source of the provider that manages it, such as registry.terraform.io/hashicorp/google. Changing required_providers to a fork and running terraform init installs the fork but leaves those records unchanged, so planning fails while state still names a provider the configuration no longer requires.

terraform state replace-provider \
  -auto-approve \
  hashicorp/google registry.example.net/platform/google

terraform state replace-provider FROM TO rewrites the provider address of every resource that uses FROM, in one step. The first argument is the provider in state now, the second is the replacement; reversed, nothing matches. It asks for confirmation unless you pass -auto-approve (needed in a pipeline) and writes a state backup. init -upgrade only selects newer versions for the lock file, refresh only updates attribute values, and state mv works on resource addresses, so none of them change the recorded provider. Removing and re-importing every resource also ends in correct state but takes far more steps and risk.

How do -replace, taint and untaint handle a degraded object?

To rebuild one object whose configuration has not changed, use terraform plan -replace=ADDRESS (or apply -replace). Terraform plans a replace action for that address even when the configuration would cause no action, and the replacement appears in a plan you review first. Quote keyed addresses for the shell: -replace='aws_lambda_function.worker["ingest"]'.

terraform taint is deprecated in favour of -replace: it edits state immediately to mark the object tainted, outside any reviewed plan, and other runs can pick that up before you meant them to. -target does not force replacement; it only limits what is planned.

Terraform also taints objects itself. If a multi-step create fails partway, typically a creation-time provisioner erroring, the object is marked tainted and the next plan proposes to replace it. When you have confirmed the object is actually healthy, terraform untaint ADDRESS clears the flag in state without touching the real object. A refresh does not clear taint (it updates attribute values only), and state rm orphans the object and plans a new one.

How does HCP Terraform detect drift with health assessments?

HCP Terraform health assessments run on a schedule without anyone queuing a run: drift detection compares real infrastructure with the workspace's configuration, and continuous validation re-evaluates check blocks and conditions. They run a speculative plan, so they never change infrastructure, and results appear on the workspace's Health page.

Enablement. Workspace admins can turn assessments on per workspace; an organization owner can enforce them for all eligible workspaces, which overrides the workspace setting so no workspace can opt out.

Eligibility. A workspace is assessed only when:

  • it uses remote or agent execution (local execution mode is not eligible);
  • it runs a supported Terraform version (0.15.4 or later for drift detection, 1.3.0 or later for continuous validation);
  • it has applied successfully at least once (with no real infrastructure there is nothing to assess), and its latest run succeeded; after an errored, canceled or discarded run, assessments pause until a run applies successfully.

The VCS-driven workflow and the auto-apply setting are not prerequisites. Health assessments are also a paid-edition feature, so check that your organization's HCP Terraform plan includes them.

Remediation. Detected drift is resolved in one of two ways: overwrite it by queuing a normal plan and apply from the current configuration, or accept it by changing the configuration to match reality and pushing a new configuration version. A refresh-state run updates state but leaves the configuration unchanged, so the next assessment still reports the difference, and an on-demand assessment simply re-reports it.

How do ignore_changes and ignore_tags handle expected drift?

Some drift is expected because another system owns a value after creation: scaling policies change an Auto Scaling group's desired_capacity, and a compliance tool stamps its own tags on resources. Reverting it on every apply causes outages or endless diffs, so tell Terraform to tolerate it.

resource "aws_autoscaling_group" "workers" {
  # ...
  desired_capacity = 3

  lifecycle {
    ignore_changes = [desired_capacity]
  }
}

ignore_changes uses the configured value at creation, then stops planning updates for the listed arguments. It works in both directions: Terraform neither reverts external changes nor applies later edits you make to that argument in configuration. To push a new baseline once, remove the argument from ignore_changes, apply, then add it back. -target still honours ignore_changes. The other lifecycle arguments (prevent_destroy, create_before_destroy, replace_triggered_by) govern destruction and replacement and do not stop an in-place update.

For tags applied by other systems across many AWS resources, use the AWS provider's ignore_tags block instead of editing every resource:

provider "aws" {
  ignore_tags {
    key_prefixes = ["compliance-scan/"]
  }
}

keys matches exact tag names; key_prefixes matches every tag starting with the prefix. It applies to every resource using that provider configuration (each aliased configuration needs its own), and drift on your own tags is still reported. Ignoring the whole tags argument per resource would hide that drift too.

Tip. Expect production scenarios built around a constraint: read-only during a freeze, no state CLI commands, every change in a reviewed plan, keep the object running, fewest steps, or secrets that must stay out of logs. Inspection items give a goal (an inventory of one module, the address behind a cloud ID, one instance's attributes, an expression to compute, a restorable backup) and offer commands that take the wrong kind of argument, sometimes with a short for_each block and quoted addresses to build. Drift items describe a console change, deletion or autoscaler and ask what the next plan proposes, how to accept or revert it, which pipeline flags hide it, and how expected drift is tolerated per resource or across a provider. State-surgery items cover removed versus state rm, moved block direction and count-to-for_each moves, state mv with -dry-run, -replace versus taint and untaint after a failed provisioner, state push lineage and serial refusals, and replace-provider argument order. HCP Terraform items cover health assessment enforcement, eligibility and the two remediation paths.

Key takeaways
  • state list prints addresses (filter by prefix or -id), state show prints one instance's attributes, show takes a file path not an address, and output reads root outputs only.
  • for_each keys are quoted strings attached to the segment that repeats; wrap the whole address in single quotes on Unix shells.
  • show -json, output -json and output -raw print sensitive values in plain text; sensitive_values in the JSON marks what to strip.
  • terraform console evaluates expressions against state and holds a state lock while open; exit it instead of force-unlocking.
  • state pull is the backup; state push refuses a different lineage or a higher remote serial, and -force overwrites without merging.
  • apply -refresh-only accepts drift into state only; update the configuration too, or the next normal apply reverts it or recreates a deleted object.
  • A normal plan and apply reverts drift; -refresh=false and state-only views cannot see it.
  • removed blocks (destroy = false) forget objects in a reviewed plan but cannot target one instance key; state rm needs the block deleted as well.
  • moved blocks go from old to new address; count to for_each needs one per instance. Prefer -replace over taint, and untaint a healthy object tainted by a failed provisioner.
  • Health assessments need remote or agent execution and a successful latest run; org enforcement overrides workspaces; fix drift by plan and apply or by changing configuration.

Frequently asked questions

What is the difference between terraform state show and terraform show?

terraform state show takes one resource instance address and prints every attribute Terraform recorded for that instance. terraform show prints the whole latest state snapshot, or a saved plan or state file whose path you pass; its argument is a file path, never a resource address.

Does terraform show -json hide sensitive values?

No. terraform show -json prints sensitive values from state in plain text; only human-readable output replaces them with (sensitive value). The JSON includes a sensitive_values structure for each resource that marks which attributes are sensitive, so a pipeline can remove them before logging. terraform output -json and output -raw also print sensitive outputs in plain text.

How do I accept drift in Terraform without changing infrastructure?

Run terraform apply -refresh-only and approve it to record the real values in state without proposing any infrastructure changes, then update the configuration to the new values. If you skip the configuration change, the next normal apply reverts the object to the configured values, or recreates an object that was deleted.

Why does terraform plan -refresh=false miss drift?

With -refresh=false Terraform skips reading the real objects and plans against the stored state. A change made outside Terraform is therefore invisible: state and configuration still agree, so the plan can report no changes while the real resource differs.

When should I use a removed block instead of terraform state rm?

Use a removed block with lifecycle destroy = false when you want Terraform to forget a whole resource through a normal, reviewable plan; HashiCorp recommends it. Use terraform state rm when you need to forget a single instance of a count or for_each resource, because a removed block cannot take an instance key. In both cases the resource block (or the instance's key) must leave the configuration, or Terraform plans to create it again.

Is terraform taint still recommended?

No. terraform taint is deprecated; use terraform plan or apply with -replace=ADDRESS, which forces replacement of one object inside a plan you review first. terraform untaint is still used to clear the tainted flag Terraform sets when a create-time step such as a provisioner fails and you have confirmed the object is healthy.

Why is an HCP Terraform workspace not getting health assessments?

Health assessments need a workspace using remote or agent execution, a supported Terraform version (0.15.4 or later for drift detection, 1.3.0 or later for continuous validation) at least one successful apply and a successful latest run. Local execution mode or an errored, canceled or discarded latest run stops assessments even when an organization owner enforces them.

How do I change the provider source recorded in Terraform state?

Run terraform state replace-provider with the current provider source first and the new source second, for example terraform state replace-provider hashicorp/google registry.example.net/platform/google. It updates every resource using the old provider and writes a backup; add -auto-approve for non-interactive pipelines. terraform init -upgrade and refresh do not change the recorded provider.

Source

This lesson covers the "Terraform state and backend operations" domain of the official Terraform Pro 002 exam guide. Vendors revise their guides — check the source for the current version.

Sign up free to mark lessons complete, bookmark topics and track your exam readiness.

Spotted a mistake in this lesson?