08 · Disaster Recovery for State & Infra¶
Level 3's module on refactoring state safely covered surgical, intentional state edits. Disaster recovery is the opposite scenario: something has already gone wrong — a state file was deleted, corrupted, or an entire region went dark — and the question is how to get back to a known-good, working configuration with the least additional damage.
The state file is the single point of failure state operations create¶
terraform {
backend "s3" {
bucket = "acme-tfstate"
key = "production/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "acme-tf-locks"
}
}
Lose terraform.tfstate — accidentally delete the S3 object, corrupt it
with a bad manual edit, or have the bucket itself destroyed — and
Terraform loses all memory of which real-world resources it's supposed
to be managing. The infrastructure keeps running (state loss doesn't
touch actual cloud resources), but the next terraform plan sees an
empty state against a populated configuration and proposes creating
everything from scratch, which would either fail loudly (most cloud
resources reject creating something that already exists, like an S3
bucket name collision) or, worse, some resources would succeed and
silently duplicate infrastructure. This is why state is the actual
disaster-recovery-critical asset in a Terraform-managed environment —
not the .tf files, which are just text in version control and trivially
recoverable.
Line of defense one: versioned, replicated state storage¶
resource "aws_s3_bucket" "tfstate" {
bucket = "acme-tfstate"
}
resource "aws_s3_bucket_versioning" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_replication_configuration" "tfstate" {
role = aws_iam_role.replication.arn
bucket = aws_s3_bucket.tfstate.id
rule {
id = "replicate-to-dr-region"
status = "Enabled"
destination {
bucket = "arn:aws:s3:::acme-tfstate-dr-west"
}
}
}
Versioning on the state bucket means an accidental delete or a bad overwrite is recoverable — S3 keeps every prior version of the object, so restoring is "promote the previous version," not "reconstruct from scratch." Cross-region replication protects against losing the entire bucket or region, not just one object version. Neither of these is optional hardening for a production Terraform setup; they're the baseline that makes every other recovery step in this module possible at all, because every one of them assumes some prior good state exists somewhere to recover to.
Recovering a lost or corrupted state file¶
aws s3api list-object-versions --bucket acme-tfstate --prefix production/terraform.tfstate
aws s3api get-object --bucket acme-tfstate --key production/terraform.tfstate \
--version-id abc123def456 recovered.tfstate
terraform state push recovered.tfstate
terraform state push uploads a local state file as the new remote
state, after Terraform checks it against the backend's serial/lineage
metadata to avoid silently clobbering a newer concurrent write. Recovery
is: identify the last known-good version (via S3 versioning, or a backup
taken by CI before each apply), pull it down, verify it against reality
with terraform plan (expect zero changes if the recovered state
genuinely matches deployed infrastructure), and only then push it back
as the authoritative state.
When there's no backup at all: reconstructing state from real infrastructure¶
terraform import aws_vpc.this vpc-0abc123
terraform import aws_subnet.main subnet-0def456
terraform import aws_db_instance.primary acme-prod-db
If no state backup exists, the only path back is importing every
existing resource one at a time — terraform import binds a real
resource ID to a resource address already declared in configuration,
populating that one resource's state without touching anything else.
This is slow and error-prone at scale (dozens or hundreds of resources,
each needing the exact right address and ID), which is precisely the
argument for why versioned/replicated state storage isn't optional: the
alternative is a manual, resource-by-resource reconstruction under
incident pressure, with real risk of missing a resource entirely and
having Terraform propose destroying something still in production use.
Worked example: recovering from a region outage with a warm-standby workspace¶
# dr/main.tf — separate state, separate workspace, deployed to us-west-2
terraform {
backend "s3" {
bucket = "acme-tfstate"
key = "production-dr/terraform.tfstate"
region = "us-west-2"
}
}
module "app_stack" {
source = "../modules/app-stack"
region = "us-west-2"
# sized down: standby capacity, scaled up only during failover
instance_count = 1
}
# Failover: scale up the DR workspace, then redirect traffic
terraform apply -var="instance_count=6" -target=module.app_stack
A genuinely resilient DR plan doesn't rely solely on state recovery — it maintains a second, independently-stated deployment of the same application stack in another region, kept minimally provisioned during normal operation and scaled up on failover. This is architecturally distinct from state recovery: it's accepting the primary region's state and infrastructure both might become unavailable simultaneously, and having a pre-tested, separately-applied configuration ready to take over rather than depending on recovering anything from the failed region at all.
How It Actually Works¶
S3 object versioning works at the storage layer, entirely below
Terraform's own state-locking and serial mechanisms — every PUT to the
state object creates a new immutable version with its own version ID,
regardless of what wrote it or whether that write went through
Terraform at all. This is why it protects against failure modes
Terraform's own mechanisms don't cover: a locking bug or a bypassed lock
(someone running an old Terraform binary that doesn't support the
configured backend's lock protocol) can still produce a bad state write,
but it can never destroy the previous version, because versioning is
enforced by the storage backend, underneath and independent of anything
Terraform Core does.
terraform state push isn't a raw overwrite — Terraform Core reads the
target backend's current state, compares its serial (a monotonically
incrementing counter embedded in every state file, bumped on every
write) and lineage (a UUID identifying which "family" of state history
a file belongs to, set once at state creation and never changed) against
the state being pushed, and refuses the push if the serial suggests it
would overwrite work newer than what you're pushing from. This is the
exact mechanism (introduced in Level 3's refactoring-state module for
state mv) doing double duty here: the same lineage/serial check that
prevents two people's concurrent applys from silently clobbering each
other also prevents a stale recovered backup from silently clobbering a
newer, valid state during a botched recovery attempt.
terraform import populates exactly one resource's entry in state by
calling the provider's ReadResource RPC against the given real-world
ID and storing whatever attributes come back — it does not, and cannot,
infer which configuration block a given cloud resource should bind to,
which is why the resource address has to already exist in configuration
before you import into it. This is a direct consequence of the
provider-protocol boundary covered across this course: state is
Terraform Core's local record of "what does resource address X
currently look like in the real world," and import is simply Core
asking the provider to answer that question for one specific ID and
recording the answer — it never runs the graph, never plans, never
touches any other resource, which is exactly why full reconstruction
requires one import call per resource with no bulk shortcut.
Exercise¶
Your team's S3 state bucket has versioning enabled but no
cross-region replication, and an engineer just accidentally ran aws s3
rb --force against the bucket, deleting it and every version in it.
Walk through what recovery options remain, and explain — referencing the
lineage/serial mechanism above — what Terraform would do differently if
you instead tried to recover by writing a freshly generated state file
(from terraform import-ing everything) versus recovering an actual
prior version of the deleted file.