Skip to main content

What is Terraform State?

Terraform state is a record of the infrastructure that Terraform has created and is managing. It serves as the source of truth for what exists, enabling Terraform to determine what changes need to be made to match your configuration.
State is the bridge between your declarative configuration and the real infrastructure in your cloud provider. Without state, Terraform wouldn’t know which resources it manages or what their current configuration is.

The State Structure

State is represented by the states.State struct:
type State struct {
    // Modules contains state for each module instance
    Modules map[string]*Module
    
    // Root module outputs (only root outputs are persisted)
    RootOutputValues map[string]*OutputValue
    
    // Check results from the last run
    CheckResults *CheckResults
}
Each module contains:
type Module struct {
    Addr addrs.ModuleInstance
    
    // Resources in this module
    Resources map[string]*Resource
    
    // Output values (child modules only during execution)
    OutputValues map[string]*OutputValue
    
    // Local values (ephemeral, not persisted)
    LocalValues map[string]cty.Value
}
Source: states/state.go and states/module.go

What State Contains

Resource Instances

For each managed resource, state tracks:
  • Resource address - Unique identifier (e.g., aws_instance.web[0])
  • Provider - Which provider manages this resource
  • Attributes - Current attribute values
  • Dependencies - Recorded dependencies
  • Metadata - Creation time, schema version, etc.
{
  "resources": [
    {
      "mode": "managed",
      "type": "aws_instance",
      "name": "web",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "attributes": {
            "id": "i-1234567890abcdef0",
            "ami": "ami-123456",
            "instance_type": "t2.micro",
            "public_ip": "54.123.45.67"
          },
          "dependencies": [
            "aws_security_group.web"
          ]
        }
      ]
    }
  ]
}

Output Values

Root module outputs are persisted:
{
  "outputs": {
    "instance_ip": {
      "value": "54.123.45.67",
      "type": "string"
    }
  }
}
Child module outputs are calculated during execution but not persisted. Only root module outputs survive between runs.

State Metadata

State files include metadata:
  • Terraform version - Version that last wrote the state
  • Serial - Incremented on each write (prevents conflicts)
  • Lineage - UUID identifying this state lineage (prevents mixing states)
  • Timestamp - When state was last modified

State Managers

State is accessed through state managers that implement the statemgr interfaces:
// Full state manager interface
type Full interface {
    Locker
    Reader
    Writer
    PersistentMeta
    Refresher
}
Source: statemgr package

Filesystem State Manager

The default statemgr.Filesystem writes state to local files:
terraform.tfstate         # Current state
terraform.tfstate.backup  # Previous state

Remote State Managers

Remote backends provide state managers that store state remotely:
  • S3 - Stores in AWS S3 bucket
  • Azure Blob - Stores in Azure Storage
  • GCS - Stores in Google Cloud Storage
  • Terraform Cloud - Stores in HashiCorp’s service
  • Consul - Stores in HashiCorp Consul
Each backend implements its own state manager that handles:
  • Reading from remote storage
  • Writing to remote storage
  • Locking to prevent concurrent modifications
  • Refreshing to get latest version

State Locking

State locking prevents concurrent modifications:
// Acquire lock before operations
func (s *State) Lock(info *LockInfo) error

// Release lock after operations
func (s *State) Unlock() error
If Terraform crashes while holding a lock, you may need to manually remove it:
terraform force-unlock <LOCK_ID>
Only do this if you’re certain no other Terraform process is running.

Lock Information

type LockInfo struct {
    ID        string    // Unique lock ID
    Operation string    // What operation holds the lock
    Who       string    // User/process that acquired lock
    Created   time.Time // When lock was acquired
}

Thread-Safe State Access

During graph walks, multiple vertices may access state concurrently. Terraform uses states.SyncState to provide thread-safe access:
type SyncState struct {
    state *State
    lock  sync.RWMutex
}

// Thread-safe reads
func (s *SyncState) Lock()
func (s *SyncState) Unlock()

// Atomic operations
func (s *SyncState) SetResourceInstanceCurrent(...)
func (s *SyncState) Resource(...) *Resource
Source: states/sync.go
The graph walker creates a SyncState wrapper around the state, which all vertex executions use. This ensures concurrent read/write operations don’t corrupt the state.Source: docs/architecture.md

State Lifecycle

State evolves through the Terraform workflow:
1

Previous Run State

State as it was saved at the end of the last Terraform operation.
2

Upgraded State

Provider upgrades state to current schema version via UpgradeResourceState.
3

Prior State (Refreshed)

Provider reads current remote state via ReadResource to detect drift.
4

Planning

Terraform calculates difference between Prior State and Configuration.
5

New State

Provider returns new state after applying changes via ApplyResourceChange.
6

Persist

New state is written to storage, becoming the Previous Run State for next time.
Source: docs/resource-instance-change-lifecycle.md

State Refresh

Refreshing updates state from the real infrastructure:
# Refresh state without planning changes
terraform refresh

# Or as part of plan (default)
terraform plan

# Skip refresh during plan
terraform plan -refresh=false

Refresh Process

1

Upgrade State

Call provider’s UpgradeResourceState to handle schema migrations.
2

Read Resources

Call provider’s ReadResource for each resource in state.
3

Detect Drift

Compare values returned by provider with values in state.
4

Update State

Update state with current values from provider.
Source: docs/resource-instance-change-lifecycle.md

Drift Detection

Providers must distinguish between: Normalization - Same value in different format:
// State has: {"tags": {"Name": "web", "Env": "prod"}}
// Provider returns: {"tags": {"Env": "prod", "Name": "web"}}
// Action: Return state value (preserve user's format)
Drift - Actual change in remote system:
// State has: {"instance_type": "t2.micro"}
// Provider returns: {"instance_type": "t2.small"}
// Action: Return provider value (record the drift)
Use -refresh=false to skip refresh when you know state is current. This speeds up planning but may miss drift.

State Snapshots

Every state write creates a snapshot:

State Versions

The state file format has evolved:
  • Version 1-3 - Legacy formats (Terraform < 0.12)
  • Version 4 - Current format (Terraform >= 0.12)
State snapshots include the version number:
{
  "version": 4,
  "terraform_version": "1.6.0",
  "serial": 42,
  "lineage": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  ...
}

Lineage Tracking

Lineage prevents accidentally mixing different states:
type StateStore struct {
    // Lineage UUID - randomly generated on first write
    Lineage string
}
If Terraform detects a lineage mismatch, it will refuse to continue:
Error: state lineage mismatch

This state was created for a different configuration.
This usually means you’re using the wrong state file for this configuration.

State Commands

Terraform provides commands for state manipulation:

List Resources

# List all resources in state
terraform state list

# Output:
# aws_vpc.main
# aws_subnet.app[0]
# aws_subnet.app[1]
# aws_instance.web

Show Resource

# Show detailed state for a resource
terraform state show aws_instance.web

# Output:
# resource "aws_instance" "web" {
#     id            = "i-1234567890abcdef0"
#     ami           = "ami-123456"
#     instance_type = "t2.micro"
#     ...
# }

Move Resources

# Rename resource in state
terraform state mv aws_instance.web aws_instance.app

# Move to a module
terraform state mv aws_instance.web module.compute.aws_instance.web

Remove Resources

# Remove from state (doesn't destroy)
terraform state rm aws_instance.web
terraform state rm removes the resource from state but doesn’t destroy the real infrastructure. Use this when you want Terraform to stop managing a resource.

Import Resources

# Import existing resource into state
terraform import aws_instance.web i-1234567890abcdef0
Import calls the provider’s ImportResourceState function.

Remote State

Remote state enables collaboration:
terraform {
  backend "s3" {
    bucket = "my-terraform-state"
    key    = "prod/terraform.tfstate"
    region = "us-west-2"
    
    # Enable state locking
    dynamodb_table = "terraform-locks"
  }
}

Benefits of Remote State

Collaboration

Multiple team members can work with the same state

Locking

Prevents concurrent modifications and race conditions

Backup

State is stored durably in cloud storage

Encryption

Sensitive data can be encrypted at rest

State Sharing

Use terraform_remote_state data source to reference another state:
data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "my-terraform-state"
    key    = "network/terraform.tfstate"
    region = "us-west-2"
  }
}

resource "aws_instance" "app" {
  subnet_id = data.terraform_remote_state.network.outputs.subnet_id
}

Sensitive Data in State

State files may contain sensitive data:
  • Database passwords
  • API keys
  • Private keys
  • Any value marked as sensitive
Never commit state files to version control!Add to .gitignore:
*.tfstate
*.tfstate.*

Securing State

Store state in encrypted remote storage:
backend "s3" {
  bucket = "my-terraform-state"
  key    = "terraform.tfstate"
  encrypt = true  # Server-side encryption
}
Use backends that support locking to prevent corruption:
backend "s3" {
  # ... other config ...
  dynamodb_table = "terraform-locks"
}
Use IAM policies to limit who can read/write state:
{
  "Effect": "Allow",
  "Action": ["s3:GetObject", "s3:PutObject"],
  "Resource": "arn:aws:s3:::my-terraform-state/*",
  "Condition": {
    "StringEquals": {"aws:PrincipalOrgID": "o-xxxxx"}
  }
}
Terraform Cloud provides:
  • Encrypted state storage
  • State versioning
  • Access controls
  • Audit logs

State Versioning

Many remote backends support versioning:
terraform {
  backend "s3" {
    bucket = "my-terraform-state"
    key    = "terraform.tfstate"
    
    # Enable versioning on the bucket
    versioning = true
  }
}
Versioning enables:
  • Rollback to previous states
  • Audit trail of changes
  • Recovery from corruption

State Migration

Migrate state between backends:
1

Configure New Backend

Update backend block in configuration:
terraform {
  backend "s3" {
    bucket = "new-state-bucket"
    key    = "terraform.tfstate"
  }
}
2

Initialize Migration

Run terraform init to trigger migration:
terraform init -migrate-state
3

Confirm Migration

Terraform will prompt for confirmation before copying state.
4

Verify

Verify new backend has complete state:
terraform state list

State Backup

Terraform automatically creates backups:

Local Backups

When using local state:
terraform.tfstate         # Current
terraform.tfstate.backup  # Previous

Remote Backups

When using remote state with versioning:
  • Each state write creates a new version
  • Old versions are retained according to backend policy
Regularly back up your state files externally, especially before major changes:
terraform state pull > backup-$(date +%Y%m%d-%H%M%S).tfstate

Workspace State

Workspaces have separate states:
# Create workspace
terraform workspace new dev

# Switch workspace
terraform workspace select prod

# List workspaces
terraform workspace list
State Storage by Workspace:
Local:
  terraform.tfstate.d/
    dev/
      terraform.tfstate
    prod/
      terraform.tfstate

S3:
  env:/dev/terraform.tfstate
  env:/prod/terraform.tfstate
Source: docs/architecture.md

Best Practices

Use remote state with locking when collaborating:
terraform {
  backend "s3" {
    bucket         = "terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "us-west-2"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}
Always enable versioning on state storage to enable recovery.
Use terraform state commands instead of editing state files directly.Manual edits can corrupt state or break Terraform’s assumptions.
Create a state backup before:
  • Major refactoring
  • Version upgrades
  • Backend migrations
terraform state pull > backup.tfstate
Large state files (>10MB) can slow Terraform operations.Consider:
  • Splitting into multiple state files
  • Using modules with separate state
  • Removing unused resources

Troubleshooting

State Locked

Error: state locked
Solution:
  1. Wait for other Terraform process to complete
  2. If process crashed, force unlock:
    terraform force-unlock <LOCK_ID>
    

State Corruption

Error: state snapshot was created by Terraform v1.5.0,
but this is v1.6.0
Solution:
  1. Restore from backup
  2. Upgrade Terraform version
  3. Run terraform state replace-provider if needed

Drift Detected

Note: Objects have changed outside of Terraform
Solution:
  1. Review changes with terraform plan
  2. Accept drift with terraform apply
  3. Or use ignore_changes to accept permanently

Next Steps

Core Concepts Overview

Return to the overview to explore other core concepts

References

Build docs developers (and LLMs) love