Australian cloud for business software — built and run in Sydney since 2003

NetLeverage
NetLeverage

Terraform state across clouds: drift diagnosis and recovery planning

Technical article
Cloud

About this article: AI-assisted writing, reviewed against the cited sources. This is source research, not a lab validation of the procedures. Check the stated versions and your own recovery requirements before making changes.

Keep one state per architectural boundary, in a backend with locking and object versioning turned on. Find drift with a refresh-only plan. Bring existing objects under management with import blocks. When state is damaged, stop writers, preserve evidence and identify candidate versions. A qualified owner must validate the recovery method before any state replacement or infrastructure apply.

The order matters because state is the record of which real object belongs to which resource block. A wrong record can turn a routine plan into a plan to destroy or duplicate production objects.

Terraform 1.17.1 was the current release when this was checked on 10 November 2026. The commands come from HashiCorp, AWS, Microsoft and Google references, and the recovery discussion is a planning framework, not a validated procedure. None was run in a lab.

Why it matters

Terraform's state holds the bindings between objects in a remote system and resource instances in your configuration. It also keeps dependency metadata, so that Terraform can work out destruction order after a resource has left the configuration.

Terraform expects each remote object to be bound to only one resource instance. Break that, and HashiCorp warns that Terraform "may behave unexpectedly".

State is also a secrets store. It can contain values such as initial database passwords or API tokens, and local state is a plaintext file. Anyone who can read state may be able to read those values, and every recovery copy made below carries the same exposure.

How to split state across clouds

The HashiCorp pages checked set no rule limiting a state to one provider. Their guidance points away from large shared states all the same.

Terraform refreshes state against real infrastructure before an operation. Editorial reading: in a state spanning three clouds, a plan needs working credentials and API access to all three, and a problem at one provider can block a change intended for another. A provider-aligned split keeps those failures apart.

Splitting creates a sharing problem. The terraform_remote_state data source exposes only outputs, but its user must have access to the entire state snapshot. HashiCorp recommends publishing shared values to a separate location where possible, so that they can have different access controls.

The backend is also a dependency. It lives in one cloud, whatever it manages. Editorial reading: if the state for Azure resources sits in S3, an S3 or AWS identity problem stops Azure changes as well. Decide that deliberately, and record where each state lives.

Backend settings that decide your recovery options

Backend Locking Previous versions Check on the page
s3 Opt-in. Set use_lockfile = true, which defaults to false HashiCorp "highly" recommends Bucket Versioning for state recovery DynamoDB locking is deprecated and will be removed in a future minor version
azurerm Supported with Azure Blob Storage native capabilities Not covered on the backend page. Blob versioning is a storage account feature Avoid credentials in -backend-config
gcs Supported HashiCorp "highly" recommends Object Versioning for state recovery The bucket must already exist. State encrypted with a lost key is not recoverable

A minimal S3 backend with locking, written for this article from the S3 backend reference:

terraform {
  backend "s3" {
    bucket       = "example-terraform-state"
    key          = "network/aws/terraform.tfstate"
    region       = "ap-southeast-2"
    encrypt      = true
    use_lockfile = true
  }
}

With use_lockfile, the identity needs s3:GetObject, s3:PutObject and s3:DeleteObject on the lock file, such as .../key.tflock. Editorial reading: a role that can read and write state but cannot delete the lock file is a misconfiguration to check first.

Two details affect retention.

  • Azure warns that with blob versioning on, every write creates a new version, and suggests lifecycle management to control the cost.
  • Google recommends soft delete instead of Object Versioning to protect against permanent loss from deletion. HashiCorp's backend page recommends Object Versioning for state recovery. The two address different events: versioning keeps a noncurrent version each time an object is replaced, which is what a bad state write does. Google also warns: "Objects can only be recovered from a deleted bucket when soft delete is enabled. Object Versioning does not provide protection against bucket deletions." Editorial recommendation: enable Object Versioning for state, and keep soft delete on so that the loss of the state bucket is also covered.

Never store state in version control or in storage without locking and access control. HashiCorp warns that this can cause data loss or expose secrets.

To move state between backends, terraform init -migrate-state attempts to copy it to the new backend. -reconfigure is the other option for a changed backend configuration, and it does not copy state. Take a copy first, and confirm with a plan afterwards.

Locking: what it protects and how to clear it

If the backend supports it, Terraform locks state for all operations that could write state. If locking fails, Terraform does not continue.

HashiCorp documents force-unlock, but limits its use to your own lock when automatic unlocking failed. Its command does not directly change infrastructure; the danger is permitting two writers to work against one state. This article supplies no force-unlock instruction.

For diagnosis, retain the lock ID, owner, operation, creation time and error. Check the actual pipeline or workstation for evidence that the owning run has ended. An old timestamp alone does not prove a run is dead. A qualified owner must resolve uncertainty and review the current vendor procedure before releasing a lock. If ownership cannot be proved, stop recovery changes and escalate with the recorded evidence.

Drift: find it without changing anything

Drift means real infrastructure no longer matches what state records. By default, terraform plan updates state in memory before calculating changes, so drift and intended changes can appear together.

Use refresh-only mode to look at drift alone:

terraform plan -refresh-only
terraform plan -refresh-only -detailed-exitcode

With -detailed-exitcode, exit code 0 means an empty diff, 1 an error and 2 a non-empty diff. That makes a scheduled drift check possible. Treat 1 as a failed check, not as "no drift".

Finding Editorial choice Effect
The manual change is wrong Run a normal plan and apply Puts the object back to the configuration
The manual change is right, such as an incident fix Update the configuration, then plan until it is clean Configuration and object agree
The state record needs updating to match reality Review, then terraform apply -refresh-only Changes state only, not real objects

terraform refresh is deprecated. It is effectively terraform apply -refresh-only -auto-approve, and it overwrites state without showing the updates.

Avoid -refresh=false during diagnosis, because it ignores external changes and can produce an incomplete or incorrect plan.

Two limits apply. A refresh reads objects recorded in state, so a resource created by hand and never imported will not appear as drift. That is an editorial reading of how refresh works. And HCP Terraform health assessments, which can run drift detection, are available in the Standard and Premium editions, need Terraform 0.15.4 or later (1.3.0 or later with continuous validation), need remote or agent execution and pause after an errored, cancelled or discarded latest run.

Importing existing objects

Use an import block for anything that exists but is not in state. The import workflow is: add a resource block, add an import block, run a plan, check it, then apply.

import {
  to = aws_s3_bucket.logs
  id = "example-log-bucket"
}

resource "aws_s3_bucket" "logs" {
  bucket = "example-log-bucket"
}

The example is written for this article. Import ID formats vary by resource, so take them from the provider's documentation.

For renames and removals, use configuration where possible, so the change goes through a reviewed plan.

Task Preferred Direct command Note
Rename or move an address moved block (module moves need v1.1 or later) terraform state mv HashiCorp warns colleagues must not change anything between the configuration edit and the command, or a plan may destroy the object
Stop managing an object removed block with lifecycle { destroy = false } terraform state rm The object continues to exist. state mv and state rm both accept -dry-run

Assessing a damaged state and preparing recovery

A partial apply and damaged state are different incidents. Terraform records completed changes and does not roll them back automatically. For an ordinary apply failure, investigate the reported cause and review a fresh plan; do not assume that an older state is the correct answer. For wrong, overwritten, missing or unreadable state, the immediate task is to preserve the evidence needed to choose a safe recovery method.

Stop writes and establish ownership

Pause automated and manual runs against the affected workspace. Record the backend, bucket or container, object key, workspace and exact Terraform and provider versions. Confirm these against the configuration used by the last successful run. A plausible state filename is not enough to distinguish similarly named environments. Retain the failed-run logs and any lock details, and establish who owns the incident.

A held lock is not permission to bypass locking. Prove that its writer has ended before a qualified owner considers the vendor-supported release procedure. If that cannot be established, leave the protection in place and investigate. This gate prevents a recovery operation from racing a still-active writer.

Preserve evidence before choosing a candidate

The state command documentation describes backups made by state-modifying operations, and local state can also have a terraform.tfstate.backup file. Availability depends on what ran and where. Look for recorded backups on the original runner and previous backend object versions; do not assume a backup exists simply because Terraform supports it.

If the backend is readable, an authorised owner can use the documented state pull operation to obtain its current state. Keep such exports outside shared logs and repositories: state can contain plaintext secrets. Record the export’s origin, timestamp and checksum in an access-controlled evidence record. Preserve the provider’s original object version as well as any CLI-exported copy, because the CLI can update the state format it outputs.

The read-only version listings below identify candidates without replacing backend state. They use placeholder names; access to their results is still sensitive.

# AWS S3 object versions
aws s3api list-object-versions --bucket example-terraform-state --prefix network/aws/terraform.tfstate

# Azure Blob versions
az storage blob list -c tfstate --account-name examplestate --prefix network.tfstate --include v --auth-mode login

# Google Cloud Storage generations
gcloud storage ls --all-versions gs://example-terraform-state/network/default.tfstate

These are documented by the AWS listing reference, Azure blob reference and Google versioning guidance. A prefix listing may include neighbouring keys: confirm the exact object rather than selecting a row solely by recency.

Decide what a candidate can prove

The newest version is not necessarily the last correct version. Match each candidate to successful and failed run times, configuration revisions and known manual changes. Record infrastructure created, replaced or removed after that candidate. An older state may omit an object that still exists; a later plan can then propose duplicates or destructive changes.

HashiCorp’s state push documentation explains two safety checks: lineage identifies the state history, and serial represents its progression. A different lineage or a destination with a higher serial causes a normal push to refuse the write. Those refusals are evidence to investigate, not an instruction to override them. The force option disables both checks and is not a recommended recovery shortcut. Do not edit state internals to manufacture a passing comparison.

If the current destination cannot be read, stop at diagnosis. There is no reliable current-state comparison, and the push reference does not establish a safe unreadable-destination procedure. Preserve the error, object-version inventory and last-run evidence for qualified Terraform/backend support. This article does not provide a write, force or overwrite sequence for that case.

Define recovery acceptance before any write

The recovery owner must choose and independently review a backend-specific method, test it on an isolated representative state, and document a recoverable copy of every input. The plan must name the expected state history, lock ownership, credential scope, selected object version and stopping criteria. It must also define which configuration revision and provider versions will be used to assess the recovered state.

After an approved recovery, a refresh-only plan can expose recorded objects that differ from their real counterparts. A normal plan then shows proposed infrastructure changes. Neither is a complete inventory of unmanaged infrastructure. Compare both with the cloud inventory and the recorded post-candidate changes, explaining every proposed create, replace and destroy action before considering an apply.

Where usable state is unavailable, import blocks may form part of a reviewed reconstruction plan. Each remote object must bind to one correct resource address, with provider-specific import support and configuration checked. Rebuilding state one object at a time can still create dangerous plans if dependencies or configuration are wrong; it is not an automatic safe fallback.

Recovery is accepted only when the owner has an explainable plan, reconciled object inventory, working lock and access controls, and a tested future recovery path. None of those outcomes was lab validated for this article.

What to choose and why

These are editorial recommendations.

Situation Choice Reason
Multiple clouds One state per boundary and provider Keeps credentials and failures apart
Values shared between states Publish outputs to a separate location terraform_remote_state needs full state access
AWS backend today on DynamoDB locking Plan the move to use_lockfile DynamoDB locking is deprecated
Any remote backend Versioning on, with a lifecycle rule and a tested restore Recovery depends on an earlier version existing
Pipelines sharing state Set a -lock-timeout Default is immediate failure
Secrets in resources Use ephemeral values or write-only arguments where the provider supports them Available from Terraform 1.10 and 1.11 respectively
Scheduled drift reporting plan -refresh-only -detailed-exitcode Non-zero exit code can drive an alert

Risks and limits

Warning: terraform state push -force overwrites the destination state with no further safety check. A wrong file makes later plans act on false information.

Warning: terraform force-unlock while another run is active can create two writers on one state.

Warning: Backup files, state pull output and terraform show -json output can contain sensitive values in plain text. Backup files stay on disk until removed by hand.

  • A recent version may not be a good version. A bad apply also writes state, so check more than the newest entry.
  • Refresh does not see the unmanaged. Hand-built resources need an inventory check of their own.
  • Versioning has to be on before the bad write. A write made before the feature was enabled replaced the earlier state with no version kept.
  • Other tools differ. This article covers Terraform CLI. OpenTofu and HCP Terraform workspace recovery have their own documentation and are not assessed here.
  • Provider behaviour varies. Import IDs and whether a refresh sees a given attribute depend on the provider and its version.
  • Not tested here. Commands and sequences come from documentation only.

Sources

Opened 10 November 2026. Sources behind the corrections reopened 10 November 2026.