State
Terraform uses a state file to track the mapping between your configuration and real-world resources. Without state, Terraform wouldn’t know what it manages or what needs to change.
What Is State?
Section titled “What Is State?”The state file (terraform.tfstate) is JSON that records:
- Every resource Terraform manages and its current attributes.
- Resource dependencies (so Terraform knows the order for create/destroy).
- Metadata like provider versions and serial number.
{ "resources": [ { "type": "aws_instance", "name": "web", "instances": [ { "attributes": { "id": "i-0abc123def456789", "ami": "ami-0c55b159cbfafe1f0", "instance_type": "t3.micro", "public_ip": "54.123.45.67" } } ] } ]}When you run terraform plan, Terraform compares your config against this state (and optionally refreshes from the real API) to determine what changed.
Local vs Remote State
Section titled “Local vs Remote State”Local State (Default)
Section titled “Local State (Default)”State is stored as terraform.tfstate in your working directory.
- Fine for learning and solo projects.
- Not suitable for teams — no locking, no shared access, risk of conflicts.
- State contains sensitive values (passwords, keys) in plaintext.
Remote State (Recommended)
Section titled “Remote State (Recommended)”Store state in a shared, locked backend. Common options:
| Backend | Locking | Notes |
|---|---|---|
| S3 + DynamoDB | Yes (via DynamoDB) | AWS-native, most common |
| Azure Blob Storage | Yes (native) | Azure-native |
| GCS | Yes (native) | GCP-native |
| Terraform Cloud | Yes (built-in) | HashiCorp’s managed service, free tier available |
| Consul | Yes | HashiCorp’s KV store |
S3 + DynamoDB Example
Section titled “S3 + DynamoDB Example”terraform { backend "s3" { bucket = "my-terraform-state" key = "prod/network/terraform.tfstate" region = "us-east-1" dynamodb_table = "terraform-locks" encrypt = true }}The DynamoDB table provides state locking — only one person (or CI job) can modify state at a time.
Terraform Cloud Example
Section titled “Terraform Cloud Example”terraform { cloud { organization = "my-org" workspaces { name = "prod-network" } }}Terraform Cloud stores state, provides locking, and can run plans/applies remotely.
State Locking
Section titled “State Locking”When two people run terraform apply at the same time, they could corrupt state. Locking prevents this:
- Before any state-modifying operation, Terraform acquires a lock.
- If someone else holds the lock, Terraform waits or errors.
- After the operation, the lock is released.
Handling Lock Conflicts
Section titled “Handling Lock Conflicts”If you run plan or apply while another process holds the lock, Terraform exits with an error such as:
Error: Error acquiring the state lockLock Info: ID: abc12345-6789-... Path: my-bucket/env/prod/terraform.tfstate Operation: OperationTypeApply Who: user@hostWhat to do:
- Wait — If a teammate or CI job is running, wait for it to finish; the lock will be released automatically.
- Force-unlock — Only if you’re certain no other operation is running (e.g. the process crashed or was cancelled), use the lock ID from the error:
terraform force-unlock abc12345-6789-...Use force-unlock sparingly. If another apply is still in progress, force-unlocking can corrupt state.
Corrupted or Diverged State
Section titled “Corrupted or Diverged State”- Don’t edit state by hand — Prefer Terraform commands (
state mv,state rm,import) ormoved/removedblocks so Terraform stays consistent with reality. - Refresh — If state and real infrastructure drifted (e.g. someone changed resources outside Terraform),
terraform apply -refresh-only(orterraform refreshin older versions) updates state from the cloud without changing resources. - Recovery — If state is corrupted or lost, restore from a backend backup if available; otherwise you may need to re-import critical resources or recreate them. Remote backends (S3, Terraform Cloud) often keep history or versioning to help with this.
State Commands
Section titled “State Commands”terraform state list
Section titled “terraform state list”List all resources in state:
terraform state list# aws_instance.web# aws_security_group.web_sg# aws_vpc.mainterraform state show
Section titled “terraform state show”Show details of a specific resource:
terraform state show aws_instance.web# resource "aws_instance" "web" {# ami = "ami-0c55b159cbfafe1f0"# instance_type = "t3.micro"# id = "i-0abc123def456789"# public_ip = "54.123.45.67"# ...# }terraform state mv
Section titled “terraform state mv”Rename or move a resource in state (without destroying/recreating):
terraform state mv aws_instance.web aws_instance.appUseful when you rename a resource in your config and don’t want Terraform to destroy and recreate it.
terraform state rm
Section titled “terraform state rm”Remove a resource from state (Terraform forgets about it, but the real resource is untouched):
terraform state rm aws_instance.webThe resource still exists in your cloud account — Terraform just stops managing it.
terraform state pull / push
Section titled “terraform state pull / push”Download or upload the raw state file (for debugging or migration):
terraform state pull > state.json # downloadterraform state push state.json # upload (use with caution)Importing Existing Resources
Section titled “Importing Existing Resources”If you have infrastructure that was created manually (or by another tool), you can bring it under Terraform management.
terraform import
Section titled “terraform import”terraform import aws_instance.web i-0abc123def456789This adds the resource to state, but you still need to write the matching resource block in your .tf files. After importing, run terraform plan — if the plan shows no changes, your config matches the real resource.
Import Block (Terraform 1.5+)
Section titled “Import Block (Terraform 1.5+)”A declarative alternative — define the import in config:
import { to = aws_instance.web id = "i-0abc123def456789"}
resource "aws_instance" "web" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t3.micro"}Run terraform plan and Terraform will plan the import. On terraform apply, the resource is imported into state.
Generating Config From Import
Section titled “Generating Config From Import”terraform plan -generate-config-out=generated.tfTerraform generates a .tf file with the resource configuration based on the real resource’s attributes. Review and clean up the generated code.
Refactoring: moved and removed Blocks
Section titled “Refactoring: moved and removed Blocks”moved Block
Section titled “moved Block”When you rename or restructure a resource in your config, Terraform normally sees “delete old + create new.” The moved block tells Terraform it’s the same resource:
# Old name was aws_instance.web, new name is aws_instance.appmoved { from = aws_instance.web to = aws_instance.app}
resource "aws_instance" "app" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t3.micro"}On the next terraform apply, Terraform updates state to use the new name — no destroy/recreate.
Common refactoring moves:
# Rename a resourcemoved { from = aws_instance.web to = aws_instance.app_server}
# Move a resource into a modulemoved { from = aws_instance.web to = module.app.aws_instance.web}
# Move between modulesmoved { from = module.old_app.aws_instance.web to = module.new_app.aws_instance.web}
# Rename a modulemoved { from = module.app to = module.application}After a successful apply, you can remove the moved block — it’s only needed for one transition.
removed Block (Terraform 1.7+)
Section titled “removed Block (Terraform 1.7+)”When you want to stop managing a resource without destroying it in the cloud. Like terraform state rm but declarative:
removed { from = aws_instance.legacy
lifecycle { destroy = false }}On the next apply, Terraform removes the resource from state but leaves the real resource untouched. This is safer than terraform state rm because it goes through the normal plan/apply workflow and is visible in code review.
With destroy = true (default), the resource is both removed from state and destroyed in the cloud — equivalent to just deleting the resource block.
terraform_remote_state Data Source
Section titled “terraform_remote_state Data Source”When infrastructure is split across separate state files (e.g. network layer vs app layer), terraform_remote_state lets one configuration read outputs from another:
data "terraform_remote_state" "network" { backend = "s3" config = { bucket = "my-terraform-state" key = "network/terraform.tfstate" region = "us-east-1" }}
resource "aws_instance" "web" { subnet_id = data.terraform_remote_state.network.outputs.public_subnet_ids[0] vpc_security_group_ids = [ data.terraform_remote_state.network.outputs.web_sg_id ]}The referenced state must expose the values as outputs:
output "public_subnet_ids" { value = aws_subnet.public[*].id}
output "web_sg_id" { value = aws_security_group.web.id}When to Use
Section titled “When to Use”- Layered architectures — Network team manages VPC, app team references it.
- Shared infrastructure — A central state holds common resources (VPC, DNS zones) used by multiple apps.
- Blast radius reduction — Separate states so a change to the app layer can’t break the network.
Alternatives
Section titled “Alternatives”- Data sources — Look up resources by tags/names instead of reading state. More loosely coupled but requires the resources to be discoverable.
- Terraform Cloud / Spacelift — These tools have built-in mechanisms for sharing outputs between workspaces.
Sensitive Data in State
Section titled “Sensitive Data in State”State may contain sensitive values (database passwords, API keys, etc.) in plaintext. To protect it:
- Encrypt at rest — Enable encryption on your S3 bucket, Azure blob container, or GCS bucket.
- Restrict access — Limit who can read the state backend (IAM policies, bucket policies).
- Don’t put secrets in
.tfvars— Use environment variables (TF_VAR_*), CI/CD secrets, or a secret store so secrets never live in committed files. - Mark outputs as sensitive — Use
sensitive = trueon output blocks so values aren’t printed interraform outputor plan/apply logs (they’re still stored in state):
output "db_password" { value = aws_db_instance.main.password sensitive = true}- Mark input variables as sensitive — For variables that hold secrets (e.g. passed via env or
-var), setsensitive = trueso Terraform redacts the value in plan and apply output:
variable "db_password" { description = "Database administrator password" type = string sensitive = true}Sensitive variables and outputs are still written to state in plaintext; encrypt the backend and restrict who can read it.
Key Takeaways
Section titled “Key Takeaways”- State maps your config to real resources — never delete or manually edit it.
- Use a remote backend with locking for any team or CI/CD workflow.
- Lock conflicts — If another process holds the lock, wait or (only if safe) use
terraform force-unlock LOCK_ID. For corrupted or diverged state, avoid hand-editing; use refresh or restore from backup. terraform state mvlets you rename resources without destroying them.terraform state rmremoves from state but doesn’t destroy the real resource.terraform importbrings existing resources under Terraform management.- Use
movedblocks to rename/restructure resources without destroy-recreate. - Use
removedblocks to stop managing a resource without destroying it (Terraform 1.7+). - Use
terraform_remote_stateto share outputs between separate state files (layered architectures). - State contains sensitive values — encrypt the backend, restrict access, and use
sensitive = trueon variables and outputs so secrets aren’t shown in CLI output.