What happens when the state file is corrupted or lost? In this episode we build a DR foundation with backup & remote storage versioning, and master the rescue operations: terraform state pull, manual JSON repair, and safe terraform state push.

After discussing infrastructure drift and refactoring in episode 18 — keeping code, state, and cloud aligned — in this episode we'll face the most tense scenario in the entire IaC journey: a corrupted or lost state file.
Imagine this. Thursday night, 11:40 PM. There's a production incident, and you need to fix the infrastructure immediately. You open the terminal, run terraform init, then terraform plan. What appears isn't a change plan — but a single line that makes your heart stop for a moment:
Error: Unmarshal tfstate: failed to unmarshal state file
json: cannot unmarshal object into Go struct field State.terraform_version of type string
Terraform state file is invalid and cannot be read. If this is unexpected,
please contact your support provider for assistance.Or worse: terraform init succeeds, but terraform plan shows Plan: 78 to add — even though the infrastructure already exists. That means the state file is lost or empty, and Terraform thinks there's nothing it's managing. All your production resources are considered "new". One naive terraform apply would create duplicates or destroy everything.
Why is this topic important? Because in SRE/DevOps work, "how fast you recover" determines a team's reputation more than "how well you write code". The state file is Terraform's long-term memory — losing it doesn't destroy the infrastructure, but it makes it unmanageable. In this episode you'll learn to build the right disaster recovery foundation, restore state from backups, perform terraform state pull and terraform state push operations safely, all the way to manual re-import when state can no longer be saved.
Since episode 5, we've known that the state file contains the mapping between resource blocks in the code and the real object IDs in the cloud. Inside it's recorded what Terraform owns: which bucket, which instance, which database — complete with all attributes and secret keys.
The logical consequence: if state is lost, Terraform is blind. It still knows what the code wants, but not what already exists in the cloud. The results:
terraform plan shows all resources as "to be created" (because there's no ownership record).terraform apply would create duplicate resources (because it doesn't know the old resources still exist).terraform destroy is even more dangerous — without state, it might think nothing needs to be destroyed (that's the good news), but if state is partially corrupted, it could destroy the wrong resources.Warning
The irony you must always remember: a lost state doesn't delete resources in the cloud — the resources keep living and keep being billed. So the worst-case scenario isn't "infrastructure vanishes", but "infrastructure vanishes from your control": duplication, ownership confusion, and uncontrolled costs. That's why DR here is truly about recovering control.
A fitting analogy: the state file is like a vehicle ownership certificate (BPKB). The car is still in the garage, still usable — but without the certificate, you can't prove ownership, can't sell it, and someday might fight over it with someone else who claims to own it.
The good news is, the DR foundation was already built in episode 5: a remote state backend on S3 with versioning, encryption, and DynamoDB for locking. Let's see why all three together form a layered defense:
| Layer | Mechanism | Role in DR |
|---|---|---|
| Remote storage | State on S3/GCS, not on a laptop | State doesn't disappear when a laptop breaks/is lost |
| Versioning | Every state change is stored as a version | Can rollback to an old version when state corrupts |
| Encryption | SSE-KMS | State contains secrets — must be protected at-rest |
| DynamoDB locking | Lock per execution | Prevents two processes from overwriting state simultaneously |
Here's the backend configuration that serves as the DR foundation in this series:
terraform {
backend "s3" {
bucket = "myapp-tfstate-bucket"
key = "production/infrastructure/terraform.tfstate"
region = "ap-southeast-1"
encrypt = true
dynamodb_table = "myapp-tfstate-lock"
}
}But wait — there's something easy to miss: the S3 backend doesn't automatically enable versioning. Versioning is a property of the bucket itself, and must be enabled once — ideally with Terraform (using a temporary local backend or a separate state) or via the console when first creating the bucket:
resource "aws_s3_bucket" "tfstate" {
bucket = "myapp-tfstate-bucket"
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
resource "aws_s3_bucket_public_access_block" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}With versioning enabled, every time terraform apply writes new state, the old version stays stored. If state is ever taken out (for example due to a wrong manual operation or corruption), you just restore the previous version — exactly like restoring a file from the recycle bin, but with a complete historical trace.
Tip
Don't forget DynamoDB for locking (episode 5). Locking doesn't only prevent two apply runs from overwriting state simultaneously — it also protects manual operations: if someone is running apply, another person's state pull/state push operation will be held back. Good DR is built on a tidy foundation, not on improvising while panicking.
State corruption can come from many directions — a JSON file damaged during upload, incomplete truncation by a wrong manual operation, a human editing JSON (breaking the episode 8 rule!), or a bug in a particular Terraform version. The symptoms vary:
Error: Unmarshal tfstate: failed to unmarshal state file — invalid JSON.Error: Missing resource instance key or odd address errors — partially broken structure.Plan: N to add even though the infrastructure already exists — empty/overwritten state.Plan: N to change) for no reason — corrupted attributes.The diagnosis sequence is always the same:
terraform state list # Can the resource list still be read?
terraform state show <addr> # Do the attributes of a specific resource make sense?If state list fails or the results don't make sense, don't proceed to apply. Stop. The next step is recovery — and recovery starts with understanding the two rescue commands: terraform state pull and terraform state push.
terraform state pull: Fetching & Printing Raw Stateterraform state pull fetches state from the backend and prints it to stdout in raw JSON format — exactly what's stored on S3. This is how you "open the state engine" for inspection, backup, or analysis:
terraform state pullThe output is a large JSON document starting roughly like this (truncated for readability):
Note the two key fields at the top:
serial — the state revision sequence number. The S3 backend (and most remote backends) refuses overwrites with a state whose serial is lower than the stored one. This is a built-in safeguard so old state doesn't accidentally overwrite new state.lineage — the state's unique "fingerprint". Two states from the same project must have the same lineage; if they differ, they come from different beginnings (a sign of two separate states that might conflict).The most common uses of state pull in a DR context:
Save the pull result to a file:
terraform state pull > backup-tfstate-$(date +%F).json
ls -la backup-tfstate-*.jsonImportant
The terraform state pull result file contains secrets in plaintext — database passwords, access keys, and so on (episode 15). Treat this file like a credential file: store it in a safe location (password manager, KMS-encrypted storage), give it 600 permissions, and never commit it to Git.
terraform state push: Overwriting State — With Full Cautionterraform state push is the reverse: takes state from stdin or a file, then overwrites it onto the backend. This is a very dangerous operation — without sufficient safeguards, one wrong push could overwrite healthy state with old or corrupted state.
terraform state push backup-tfstate-2026-08-01.jsonWhen is state push legitimately used?
pull).And when never:
state mv, state rm, etc.).Caution
The S3 backend blocks pushes with a lower serial, but pushes with the same or higher serial will be accepted — that's why push can overwrite newer state with an older version. Before pushing: (1) back up the current state with state pull, (2) make sure no apply/plan is running in the team, (3) prepare a rollback plan in case the push turns out wrong.
terraform state pull > state-backup.json
python3 -m json.tool state-backup.json # validate the JSON
jq '.resources | length' state-backup.jsonSometimes corruption is partial — the JSON structure is mostly intact, with only one or two resources damaged. In that case, manual repair is possible, but only with a strict procedure. Remember the rule from episode 8: never edit state manually — that rule prevents bad habits. For DR rescue, there's a controlled exception, and here are the steps:
terraform state pull > backup.json, also copy the latest S3 version.state push, then state list and plan to ensure no resource appears "lost" or "new".# 1. Back up the state to be repaired
terraform state pull > state-broken.json
# 2. Copy for editing — do NOT edit the original file
cp state-broken.json state-fixed.json
# 3. Edit state-fixed.json with an editor (only the genuinely broken parts)
# 4. Validate the JSON structure
python3 -m json.tool state-fixed.json > /dev/null && echo "JSON valid"
# 5. Push the repair result
terraform state push state-fixed.json
# 6. Final verification
terraform state list
terraform plan -detailed-exitcodeCaution
If the repair touches the resource structure (not just attributes) — for example a resource address shifts or a module changes — don't repair the JSON. It's safer to use terraform state mv to move addresses, and terraform import (episode 8) to rebuild resources from cloud IDs. Manual JSON is only for small repairs you truly understand.
import BlocksThe worst-case scenario: there's no backup (a bucket without versioning, or even a deleted bucket), and state is truly empty. Fortunately, there's one last rescue path: rebuilding state with import — exactly the technique from episode 8.
The principle: the resources in the cloud are still alive; we just need to tell Terraform who they are. Since Terraform 1.5+, the cleanest way is the declarative import block — we declare the address of every existing resource:
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.medium"
}
resource "aws_db_instance" "primary" {
identifier = "myapp-postgres"
engine = "postgres"
instance_class = "db.r6g.large"
}
import {
to = aws_instance.web
id = "i-0abcd1234efgh5678"
}
import {
to = aws_db_instance.primary
id = "myapp-postgres"
}The full recovery flow from zero:
terraform init # 1. Initialize the empty backend
terraform plan # 2. Verify "Plan: N to import"
terraform apply # 3. Execute import → state rebuilt
terraform plan -detailed-exitcode # 4. Verify: exit 0 = no driftTip
One re-import constraint: you must know the cloud ID of every resource. The sources are the cloud console or CLI commands (aws ec2 describe-instances, aws rds describe-db-instances, aws s3api list-buckets, and GCP/Azure equivalents). This is why resource inventory documentation (a list of resources with their IDs) is a real DR asset — prepare it early, not during an incident.
This checklist is a runbook that must be practiced — not just read. Keep it as a team document, and drill it (tabletop exercise) at least quarterly:
| No | DR Step | Tool / Command | Notes |
|---|---|---|---|
| 1 | Make sure the state bucket has versioning | aws s3api get-bucket-versioning | Without this, no rollback from S3 |
| 2 | Make sure the DynamoDB lock is active | terraform plan while someone else applies | It will be held waiting for the lock |
| 3 | Periodic backups of state to a second location | terraform state pull > backup.json | Weekly cron + store in separate storage |
| 4 | Detect corruption early | Scheduled terraform state list | Drift detection (episode 18) can be extended |
| 5 | Restore from the S3 version when corrupted | aws s3api get-object + restore the version | Fastest, safest |
| 6 | Restore from a file backup when needed | terraform state push backup.json | Validate the JSON first |
| 7 | Re-import when state is totally lost | import blocks + apply | Prepare the resource ID inventory |
| 8 | Final verification | terraform plan -detailed-exitcode | Exit 0 = fully recovered |
state push, do a mass state mv, or edit JSON, run terraform state pull > backup.json first. One cheap command that saves the whole team.state mv, state rm, state replace-provider are always safer than touching raw JSON.plan is clean.| Mistake | Impact | Solution |
|---|---|---|
| State bucket without versioning | No rollback when state corrupts | Enable versioning at bootstrap |
| Relying on local state | State lost when a laptop breaks | Remote state + backup (episode 5) |
state push without backing up first | Overwrites healthy state with an older version | Always pull + backup before pushing |
Panicking → apply when state is empty | Creates duplicates of production resources | Stop; diagnose; restore state first |
| Editing JSON without validation | Makes the corruption worse | python3 -m json.tool + push + verify |
| Not knowing resource IDs during re-import | Recovery stalls halfway | Prepare the ID inventory early |
| Never testing recovery | The procedure fails during a real incident | Regular quarterly tabletop exercises |
In this episode 19 you've built the last line of defense for your IaC code: disaster recovery for state. We discussed why state backup is the DR foundation, how S3 versioning + DynamoDB locking form a layered defense, the recovery procedure when state corrupts (diagnose → pull → repair → push → verify), and the last-resort rescue path of re-importing with import blocks when state is truly lost. You also got a DR runbook checklist and best practices that must be upheld as team discipline.
Key takeaways to bring home:
state pull for inspection & backup, state push for recovery — with backup and validation in both directions.import blocks (episode 8) is always the way out.You now have the entire tactical toolkit: from writing code, managing state, to rescuing it from a fire. Now it's time to assemble everything into one big picture — how all the pieces you learned from episode 0 to 19 come together in a complete enterprise IaC architecture.
In episode 20 — the closing episode of the Learn Terraform series — we'll design an end-to-end enterprise architecture: modular + Terragrunt, a centralized remote backend, passwordless OIDC authentication, secrets from Vault, an OPA policy gate, and automatic drift detection — plus a production readiness checklist and a code review guide. This is the peak of your entire journey. Stay excited!