Terraform state and remote backends
Terraform state
Terraform's memory.
Where to keep it, and how to protect it.
Words you'll meet
What state is
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = { env = "prod" }
}
aws_vpc.mainisvpc-0a1b2c3dvpc-0a1b2c3d10.0.0.0/16Terraform is askingnew IDa second network!y # aws_vpc.main will be updated in-place
!g + tags = { "env" = "prod" }
!p Plan: 0 to add, 1 to change, 0 to destroy.!r # aws_vpc.main will be created !r Plan: 1 to add, 0 to change, 0 to destroy.
What's inside state
{ "resources": [{
"type": "aws_db_instance",
"name": "main",
"instances": [{ "attributes": {
"id": "shop-db",
"username": "admin",
"password": "Sup3rS3cret!"
}}]
}]}
A trimmed-down state file
!g + password = (sensitive value)
!r "password": "Sup3rS3cret!"
Why local state breaks
Terraform's default is local state: a file on your own machine.
The fix: a shared remote backend, set up properly.
Remote state in S3
terraform {
backend "s3" {
bucket = "shop-tf-state"
key = "prod/network/terraform.tfstate"
region = "eu-west-2"
encrypt = true
use_lockfile = true
}
}
!p $ terraform init -migrate-state !m Initializing the backend... !y Do you want to copy existing state to the new backend? !m Enter a value: yes !g Successfully configured the backend "s3"!
Locking
!r Error: Error acquiring the state lock !m Lock Info: !m Who: amara@laptop !m Operation: OperationTypeApply
backend "s3" {
# bucket, key, region as before
use_lockfile = true
}
Choosing state boundaries
dev and prod · network, app and database · all in one file
When reality drifts
ingress {
from_port = 443
to_port = 443
}
ingress {
from_port = 22
to_port = 22
cidr_blocks = ["203.0.113.10/32"]
}
aws_security_group.app443from code22added by hand22removed by apply22now in code!p $ terraform plan -refresh-only !y Note: Objects have changed outside of Terraform !y # aws_security_group.app has been changed
Working with state safely
!p $ terraform state list
import {
to = aws_s3_bucket.logs
id = "shop-logs"
}
!p $ terraform state rm aws_s3_bucket.old
aws_vpc.main→vpc-0a1b2c3daws_s3_bucket.oldaws_s3_bucket.oldforgottenaws_s3_bucket.logsimportedvpc-0a1b2c3dshop-oldstill hereshop-logsbuilt by handshop-logsmanaged nowRecap
- State links your code to real things, and can hold secrets.
- Keep it in a protected shared backend: access control, encryption, versioning, locking.
- Choose state boundaries on purpose. When reality drifts, make code the truth.
- Change state only with Terraform's own tools.
Lesson map
Chapters
Hands-on reasoning
Practice lab
Build it for real: look inside stateoptional · about 10 min
You'll look inside a real state file, spring the "sensitive" trap, make Terraform forget something, and watch a lock stop a second run. This carries on from episode 1's lab folder, so do that one first.
You need: Terraform 1.4 or newer (install guide) or OpenTofu (install guide), and a terminal. No cloud account. No cost. Everything stays on your computer.
1. What does state know about?
terraform state list
Eight addresses, such as module.development.terraform_data.subnet[0] and module.prod.terraform_data.network. That's the address book.
2. Open one entry
terraform state show 'module.prod.terraform_data.subnet[0]'
You'll see its id and its value, 10.20.0.0/24.
3. The sensitive trap
Add this to main.tf, then run terraform apply:
# LAB ONLY: never put a real password in code.
variable "db_password" {
type = string
sensitive = true
default = "Sup3rS3cret!"
}
resource "terraform_data" "db" {
input = {
password = var.db_password
}
}
The plan shows password = (sensitive value). Hidden on screen.
Now search the state file for it. macOS or Linux:
grep -c "Sup3rS3cret" terraform.tfstate
Windows PowerShell:
Select-String -Path terraform.tfstate -Pattern "Sup3rS3cret"
It's there, in plain text. (In our OpenTofu 1.12.6 test it appeared twice.) This is why state must be protected. Look at the file if you like, but never edit it.
4. Make Terraform forget something
terraform state rm 'module.prod.terraform_data.subnet[2]'
terraform plan
Successfully removed 1 resource instance(s). Then Plan: 1 to add: Terraform forgot it, so it plans to create it again. With a real cloud resource, that create could make a second object, or fail because the name or ID is already taken. Run terraform apply to tidy up.
5. Watch a lock work (two terminals, same folder)
Terminal 1: start a change, and don't answer the question yet.
terraform apply -var "db_password=Changed123"
Terminal 2: try to plan at the same time.
terraform plan
Error: Error acquiring the state lock, with Lock Info showing Operation: OperationTypeApply. While terminal 1 waits, a file called .terraform.tfstate.lock.info exists. Now type no in terminal 1: the lock is released.
6. Your local safety net
Next to terraform.tfstate you'll find terraform.tfstate.backup: the previous version. S3 bucket versioning is the grown-up version of this.
7. Clean up
terraform destroy
Type yes. Everything was local, so nothing else is affected.
Tested step by step with OpenTofu 1.12.6 on 30 September 2026. The lab uses only features built into both tools. With OpenTofu, type tofu instead of terraform, and messages say "OpenTofu" instead of "Terraform". It could not be run with the Terraform program itself in the test environment.
How to use this lesson
Glossary. Tap any dotted-underlined term for a plain-English explanation. Playback pauses while you read.
Keyboard. Space or K plays/pauses, left and right arrows change chapter, and 1–4 answer a quick check.
Narration. The lesson currently uses your device's speech engine. The content is already segmented so recorded or cloned voice clips can replace it later without redesigning the lesson.