Skip to content

State

First PublishedLast UpdatedByAtif Alam

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.

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.

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.

Store state in a shared, locked backend. Common options:

BackendLockingNotes
S3 + DynamoDBYes (via DynamoDB)AWS-native, most common
Azure Blob StorageYes (native)Azure-native
GCSYes (native)GCP-native
Terraform CloudYes (built-in)HashiCorp’s managed service, free tier available
ConsulYesHashiCorp’s KV store
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 {
organization = "my-org"
workspaces {
name = "prod-network"
}
}
}

Terraform Cloud stores state, provides locking, and can run plans/applies remotely.

When two people run terraform apply at the same time, they could corrupt state. Locking prevents this:

  1. Before any state-modifying operation, Terraform acquires a lock.
  2. If someone else holds the lock, Terraform waits or errors.
  3. After the operation, the lock is released.

If you run plan or apply while another process holds the lock, Terraform exits with an error such as:

Error: Error acquiring the state lock
Lock Info:
ID: abc12345-6789-...
Path: my-bucket/env/prod/terraform.tfstate
Operation: OperationTypeApply
Who: user@host

What 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:
Terminal window
terraform force-unlock abc12345-6789-...

Use force-unlock sparingly. If another apply is still in progress, force-unlocking can corrupt state.

  • Don’t edit state by hand — Prefer Terraform commands (state mv, state rm, import) or moved/removed blocks so Terraform stays consistent with reality.
  • Refresh — If state and real infrastructure drifted (e.g. someone changed resources outside Terraform), terraform apply -refresh-only (or terraform refresh in 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.

List all resources in state:

Terminal window
terraform state list
# aws_instance.web
# aws_security_group.web_sg
# aws_vpc.main

Show details of a specific resource:

Terminal window
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"
# ...
# }

Rename or move a resource in state (without destroying/recreating):

Terminal window
terraform state mv aws_instance.web aws_instance.app

Useful when you rename a resource in your config and don’t want Terraform to destroy and recreate it.

Remove a resource from state (Terraform forgets about it, but the real resource is untouched):

Terminal window
terraform state rm aws_instance.web

The resource still exists in your cloud account — Terraform just stops managing it.

Download or upload the raw state file (for debugging or migration):

Terminal window
terraform state pull > state.json # download
terraform state push state.json # upload (use with caution)

If you have infrastructure that was created manually (or by another tool), you can bring it under Terraform management.

Terminal window
terraform import aws_instance.web i-0abc123def456789

This 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.

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.

Terminal window
terraform plan -generate-config-out=generated.tf

Terraform generates a .tf file with the resource configuration based on the real resource’s attributes. Review and clean up the generated code.

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.app
moved {
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 resource
moved {
from = aws_instance.web
to = aws_instance.app_server
}
# Move a resource into a module
moved {
from = aws_instance.web
to = module.app.aws_instance.web
}
# Move between modules
moved {
from = module.old_app.aws_instance.web
to = module.new_app.aws_instance.web
}
# Rename a module
moved {
from = module.app
to = module.application
}

After a successful apply, you can remove the moved block — it’s only needed for one transition.

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.


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:

layers/application/main.tf
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:

layers/network/outputs.tf
output "public_subnet_ids" {
value = aws_subnet.public[*].id
}
output "web_sg_id" {
value = aws_security_group.web.id
}
  • 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.
  • 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.

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 = true on output blocks so values aren’t printed in terraform output or 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), set sensitive = true so 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.

  • 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 mv lets you rename resources without destroying them.
  • terraform state rm removes from state but doesn’t destroy the real resource.
  • terraform import brings existing resources under Terraform management.
  • Use moved blocks to rename/restructure resources without destroy-recreate.
  • Use removed blocks to stop managing a resource without destroying it (Terraform 1.7+).
  • Use terraform_remote_state to share outputs between separate state files (layered architectures).
  • State contains sensitive values — encrypt the backend, restrict access, and use sensitive = true on variables and outputs so secrets aren’t shown in CLI output.