Terraform state interview questions rarely stop at the definition. "What is state?" turns into "where do you keep it?", then "the lock won't release, now what?", then "someone deleted the state file, walk me through the next hour". The questions below follow that path, from backends and locking to splitting a monolith and recovering lost state. For the wider Terraform screen, see 20 Terraform interview questions from real senior interviews; this post stays on state.
Terraform state interview questions on the basics
1. What does a state file hold, and why can't Terraform just query the cloud?
State maps each resource address in your configuration to the ID of the real object, caches that object's attributes, and records dependencies and output values. Two top-level fields matter in recovery questions: lineage, a unique ID assigned when the state is first created, and serial, which goes up every time the state is written. Terraform uses them to tell whether two snapshots share a history and which one is newer.
You can inspect state without opening the JSON:
terraform state list # every address Terraform tracks terraform state show aws_db_instance.main # cached attributes of one object terraform state pull > snapshot.tfstate # raw copy of the current state The usual follow-up: why not look everything up in the cloud? Names and tags aren't reliable identity, many types have no natural key, and Terraform must remember objects that have left the configuration, because those are the ones it should destroy.
Remote backends and Terraform state locking
2. Which remote backend would you choose, and how would you configure it?
Use the one that matches where the team runs: S3, Azure Blob Storage, Google Cloud Storage, or HCP Terraform if you also want its runs and policy checks. Whichever you pick, cover locking, versioning, encryption and read access. On S3:
terraform { backend "s3" { bucket = "acme-tfstate-prod" key = "network/terraform.tfstate" region = "eu-west-1" encrypt = true kms_key_id = "arn:aws:kms:eu-west-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" use_lockfile = true } } The details that matter:
- Locking is opt-in on S3.
use_lockfile = truemakes Terraform write a lock object next to the state, herenetwork/terraform.tfstate.tflock. S3-native locking was experimental in Terraform 1.10 and has been generally available since 1.11. DynamoDB locking (dynamodb_table) is deprecated; you can set both while you migrate, then drop the table. - Turn on bucket versioning. The backend docs recommend it for state recovery, and it is what saves you in question 8.
- Encryption and access.
encryptturns on server-side encryption of the state and lock files. With a customer-managed KMS key, the key policy becomes a second control on who can read state. - No variables in the backend block. Terraform's backend block can't reference variables, locals or data sources, so per-environment values go into partial configuration:
terraform init -backend-config=prod.s3.tfbackend. OpenTofu 1.8 and later can evaluate variables and locals there. Supply credentials through the environment, because values passed with-backend-configend up in the.terraformdirectory and in plan files.
3. An apply fails with "Error acquiring the state lock". What do you do?
Read the Lock Info block in the error first. It shows the lock ID, the operation, who holds the lock and when it was taken. Usually another run is in progress and the fix is to wait. In CI, -lock-timeout=5m makes a queued run retry instead of failing at once.
If the holder is dead, for example a CI runner killed mid-apply, first confirm that nothing is still running against that state: check the job, ask the person named in Who. Then release the lock with the ID from the error:
terraform force-unlock 9db590f1-b6fe-c5f2-2678-8804f089deba The docs reserve force-unlock for your own lock when automatic unlocking failed: unlocking a lock someone else still holds can leave you with multiple writers. -lock=false skips the protection rather than fixing anything. After a killed apply, run a plan before the next apply and read it. Objects the dead run created may never have made it into state, and a plan that wants to create something that already exists is how you find them.
4. What does state locking not protect you from?
- Two states managing the same object. A lock covers one state, not one cloud resource. If two root modules declare the same security group with different rules, each apply undoes the other's change, and neither lock prevents it.
- The gap between plan and apply. A lock lasts for a single operation, so nothing holds it between a plan reviewed on Tuesday and an apply on Thursday. Terraform guards that gap a different way: a saved plan records the state it was built from, and if anything writes state in between (another apply, an import, a
state rm), applying it fails with "Saved plan is stale" and you plan again.
5. How do you secure state, given it stores secrets in plaintext?
State and saved plan files can contain sensitive resource attributes, including generated passwords and private keys. Marking a variable or output sensitive hides it in plan and apply output; the value is still in state. The controls that work:
- Limit who can read the backend with IAM on the bucket and key prefix, plus the KMS key policy, and audit reads.
- Treat saved plan files like state. CI artifacts that keep
tfplanfiles around are a second copy of your secrets. - Keep secrets out of state where the provider supports it: ephemeral resources (Terraform 1.10) and write-only arguments (Terraform 1.11) are omitted from state and plan files.
- On OpenTofu, client-side encryption (since 1.7) encrypts state before it reaches the backend, and can encrypt plan files too.
How do you split a monolithic Terraform state?
6. One state holds everything, plans are slow and every apply touches it all. How do you split it?
Pick the boundaries first, by blast radius, ownership and how often things change. Networking, shared data stores and each service's resources are the usual cuts. Each piece becomes a root module with its own state key, and its own credentials when the boundary is an account.
Then move objects without destroying them, using import blocks in the new root and removed blocks in the old one, so every step is a plan someone can review. Check first that each resource type you move supports import; most do, and any that don't stay in the old root for now.
- Pause applies on the old root and save a copy:
terraform state pull > pre-split.tfstate. - In the new root, add the resource blocks and an
importblock for each one. The provider docs give each type's import ID format, andterraform state showin the old root has the values. - In the old root, delete those resource blocks and add
removedblocks withdestroy = false. - Plan both. The new root should show only imports and the old one only removals from state. Any create, update or destroy is a configuration mismatch to fix first.
- Apply the new root, then the old one, then resume applies.
# new root: network/imports.tf import { to = aws_vpc.main id = "vpc-0a1b2c3d4e5f67890" } # old root: removed.tf removed { from = aws_vpc.main lifecycle { destroy = false } } Import blocks need Terraform 1.5, or 1.7 for for_each across many instances; removed blocks need 1.7. Applying the import first means that for a few minutes the object sits in both states, which is harmless while applies are paused. The other order leaves it unmanaged, and if the second step gets forgotten you have an orphan.
The new roots still need each other's values. A reference like aws_vpc.main.id becomes a cross-stack lookup: the terraform_remote_state data source reads another stack's outputs, but the docs point out that whoever uses it needs access to the entire state snapshot. Publishing values to SSM Parameter Store, or looking objects up with data sources, avoids that. For one root per account in a single repository, see Terraform mono-repo design for 20 AWS accounts.
7. When do moved blocks help, and where do they stop?
Moved blocks (Terraform 1.1 and later) record a refactor inside one state: renaming a resource, moving it into a module, or switching instance keys when you replace count with for_each.
moved { from = aws_instance.web[0] to = aws_instance.web["blue"] } moved { from = aws_s3_bucket.logs to = module.logging.aws_s3_bucket.this } The plan reports each object as moved, with no destroy and create. Terraform looks up the from address in the state of the configuration that declares the block, so a moved block can't hand an object to a different state; that is the import-and-removed job from question 6. A module can only declare moves for its own objects and its child modules' objects. Changing resource type works only where the provider supports it. The moved blocks question page walks the rename case end to end.
For renames, module moves and key changes within one resource type, terraform state mv does the same job by hand, once per state. A moved block ships with the module, so every configuration using it picks up the move on its next plan. HashiCorp recommends keeping old moved blocks in shared modules, since removing one breaks anyone who hasn't applied the new version yet.
Recovering corrupted or lost Terraform state
8. The state file is corrupted or deleted. How do you recover?
Pause the pipelines first: against empty state, Terraform plans to create every resource again, and an apply would duplicate anything without a unique name.
Then recover from the backend's history. With S3 versioning, find the last good version and download it to inspect:
aws s3api list-object-versions --bucket acme-tfstate-prod \ --prefix network/terraform.tfstate aws s3api get-object --bucket acme-tfstate-prod \ --key network/terraform.tfstate --version-id <version-id> restored.tfstate If Terraform can't read the current snapshot, restore through S3 by copying the good version back over the key. terraform state push restored.tfstate works when the current state is readable or absent. It refuses a state whose lineage differs or whose serial is lower than the remote one; -force skips those two checks but not read errors. A deliberate rollback always has a lower serial, so check the lineage before you use -force. One trap on older setups: with DynamoDB locking, the table stores a checksum of the state, and an object restored behind Terraform's back fails that check until the digest is updated. HCP Terraform keeps a version history per workspace, so the same idea applies there.
A restored snapshot is only as current as its timestamp. Run a plan and read every line. Objects created after the snapshot appear as creates for things that already exist, so import those; refresh picks up later updates and deletions of tracked objects, and each of those diffs needs a decision before anyone applies.
With no backup at all, rebuild state from the configuration. Write an import block for every resource address, look up each ID in the console or CLI, and iterate on the plan until it shows only imports. A type that doesn't support import has to be recreated in a controlled change. For resources that have no configuration yet, terraform plan -generate-config-out=generated.tf writes resource blocks for you; it is still marked experimental, so review what it produces. The Terraform import question page covers the adoption workflow.
Then add the guardrail that would have made this boring. In a versioned bucket, a plain delete only adds a delete marker, and a request to remove a version for good needs s3:DeleteObjectVersion, so deny that to everyone except a break-glass role. Lifecycle rules can still expire old versions, so check they keep noncurrent state versions as long as you might need them.
9. The apply failed with "Failed to persist state to backend". What now?
Terraform changed the infrastructure but couldn't write the new state, often because credentials expired during a long apply or the backend refused the write. It saves the state it couldn't upload to errored.tfstate in the working directory, and that file is the only record of what the apply just did. Don't run another apply. Fix the access problem, push the file, then review a fresh plan against the failed run's errors and what actually exists:
terraform state pull > before-push.tfstate # what the backend holds now terraform state push errored.tfstate terraform plan # reconcile with the failed run In CI, a failed job has to keep errored.tfstate somewhere as locked down as the backend itself, since it holds the same secrets. If the runner's workspace is wiped, the file is gone and you are back to question 8, importing whatever that apply created.
Workspaces or a directory per environment?
10. Should environments use CLI workspaces or separate directories?
CLI workspaces give one configuration several states in the same backend. On S3, a staging workspace's state lands at env:/staging/network/terraform.tfstate, so every environment shares the bucket, the backend access and the code path, even when provider roles differ per workspace. Workspaces alone aren't an access boundary. They suit short-lived copies, such as a test environment per branch. For dev and prod, with different accounts, roles and approvals, a root module per environment with its own backend configuration is easier to defend. HCP Terraform workspaces are a different thing, each with its own state, variables and permissions. The Terraform workspaces question page goes through the trade-off.
Interview questions this covers
- Which backend would you use for Terraform state, and how do you lock state on S3 without DynamoDB?
- An apply can't acquire the state lock. When is force-unlock safe?
- How do you split a monolithic Terraform state without destroying or recreating anything?
- Your state file is corrupted or deleted. Walk me through the recovery.
- When do you use moved blocks, and when do you need import and removed blocks instead?
For more practice, browse all the DevOps interview ebooks.
Senior state questions go further: designing state for 200+ configurations with locking, isolation and disaster recovery, and speeding up a plan that has crept from two minutes to twenty. Infrastructure as Code: Terraform & OpenTofu Interview Questions works through 50 senior-level questions with worked answers, including a chapter on state and backends. It is one of the five books in the DevOps Interview Mastery bundle, and every purchase includes the free Interview-Day Playbook with a 7-day and 24-hour prep plan and a 30-second answer framework.
Prefer to watch or listen? Answers like these are walked through on YouTube and on the podcast, on Spotify and Amazon Music.
Originally published at devopsinterview.cloud.