Local Terraform state works for learning. Production requires remote state—for team collaboration, state locking, and not losing your infrastructure when your laptop dies. Here’s how to set it up properly.

Why Remote State?

Local state (terraform.tfstate) has problems:

  1. No collaboration - Team members overwrite each other’s changes
  2. No locking - Concurrent applies corrupt state
  3. No backup - Laptop dies, state is gone, orphaned resources everywhere
  4. Secrets in plain text - State contains sensitive data

Remote backends solve all of these.

S3 Backend (AWS)

The most common choice for AWS users:

Bootstrap Resources

First, create the backend resources (chicken-and-egg problem):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# bootstrap/main.tf - Apply this ONCE with local state
provider "aws" {
  region = "us-east-1"
}

resource "aws_s3_bucket" "terraform_state" {
  bucket = "mycompany-terraform-state"

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "aws:kms"
    }
  }
}

resource "aws_s3_bucket_public_access_block" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-state-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

Configure Backend

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# backend.tf
terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "prod/infrastructure/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-state-locks"
  }
}

State File Organization

s3://pssmrthyoaacdgro/ikdiikedmnuannudnpfbtgfb/sarea/re/narbartysnasne-testertrtertreuesuearcscsfrttt/oauteutrfrerremoerrer.r/ra/rtmtaftaf-efoefssrorrottrrmrraaam.amttf.tf.eeotfot/rfsrfmstms.ta.ttattafteftsesettaattee

Each component gets its own state file. This limits blast radius and speeds up operations.

GCS Backend (Google Cloud)

1
2
3
4
5
6
terraform {
  backend "gcs" {
    bucket = "mycompany-terraform-state"
    prefix = "prod/infrastructure"
  }
}

GCS handles locking automatically—no separate lock table needed.

Azure Backend

1
2
3
4
5
6
7
8
terraform {
  backend "azurerm" {
    resource_group_name  = "terraform-state-rg"
    storage_account_name = "mycompanytfstate"
    container_name       = "tfstate"
    key                  = "prod/infrastructure/terraform.tfstate"
  }
}

Terraform Cloud / Enterprise

1
2
3
4
5
6
7
8
terraform {
  cloud {
    organization = "mycompany"
    workspaces {
      name = "prod-infrastructure"
    }
  }
}

Includes state management, locking, run history, and policy enforcement.

Backend Configuration Patterns

Partial Configuration

Keep secrets out of code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# backend.tf
terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "prod/infrastructure/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-state-locks"
    # role_arn configured via CLI or environment
  }
}
1
2
3
4
5
6
7
# Initialize with role
terraform init \
  -backend-config="role_arn=arn:aws:iam::123456789:role/TerraformRole"

# Or use environment
export AWS_ROLE_ARN="arn:aws:iam::123456789:role/TerraformRole"
terraform init

Backend Config Files

1
2
3
4
5
6
# backend-prod.hcl
bucket         = "mycompany-terraform-state"
key            = "prod/infrastructure/terraform.tfstate"
region         = "us-east-1"
dynamodb_table = "terraform-state-locks"
role_arn       = "arn:aws:iam::123456789:role/TerraformProd"
1
terraform init -backend-config=backend-prod.hcl

Workspaces

Multiple environments, one configuration:

1
2
3
4
5
6
7
8
9
terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "infrastructure/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-state-locks"
    workspace_key_prefix = "env"
  }
}
1
2
3
4
5
terraform workspace new staging
terraform workspace new prod

terraform workspace select prod
terraform apply

State paths become:

  • env:/staging/infrastructure/terraform.tfstate
  • env:/prod/infrastructure/terraform.tfstate

State Locking

How It Works

  1. Terraform acquires lock before any state-modifying operation
  2. Lock contains: who, when, operation type
  3. Other operations wait or fail
  4. Lock released when operation completes

Viewing Locks

1
2
3
4
5
# S3/DynamoDB - check the table
aws dynamodb scan --table-name terraform-state-locks

# Force unlock (dangerous - only if lock is stale)
terraform force-unlock LOCK_ID

Lock Timeout

1
2
# Wait longer for lock
terraform apply -lock-timeout=10m

State Operations

View State

1
2
3
4
5
6
7
8
# List resources
terraform state list

# Show specific resource
terraform state show aws_instance.web

# Pull remote state locally
terraform state pull > state.json

Move Resources

1
2
3
4
5
6
7
8
# Rename resource
terraform state mv aws_instance.web aws_instance.webserver

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

# Move between state files
terraform state mv -state-out=other.tfstate aws_instance.web aws_instance.web

Remove from State

1
2
# Remove without destroying (for imports elsewhere)
terraform state rm aws_instance.legacy

Import Existing Resources

1
2
# Import existing resource into state
terraform import aws_instance.web i-1234567890abcdef0

Disaster Recovery

State Backup

S3 versioning handles this automatically:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# List state versions
aws s3api list-object-versions \
  --bucket mycompany-terraform-state \
  --prefix prod/infrastructure/terraform.tfstate

# Restore previous version
aws s3api get-object \
  --bucket mycompany-terraform-state \
  --key prod/infrastructure/terraform.tfstate \
  --version-id "abc123" \
  restored-state.json

State Recovery Process

  1. Stop all Terraform operations
  2. Download corrupted state for analysis
  3. Identify last good version from versioning
  4. Restore good version as current
  5. Run terraform plan to verify
  6. Resume operations
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Download current (corrupted) state
terraform state pull > corrupted.json

# Get previous version
aws s3api get-object \
  --bucket mycompany-terraform-state \
  --key prod/infrastructure/terraform.tfstate \
  --version-id "PREVIOUS_VERSION_ID" \
  good-state.json

# Push restored state
terraform state push good-state.json

# Verify
terraform plan

Security Considerations

Encryption

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# S3 - server-side encryption
backend "s3" {
  encrypt = true  # Uses AES-256 or KMS
}

# With specific KMS key
backend "s3" {
  encrypt    = true
  kms_key_id = "arn:aws:kms:us-east-1:123456789:key/..."
}

Access Control

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::mycompany-terraform-state/*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:DeleteItem"
      ],
      "Resource": "arn:aws:dynamodb:*:*:table/terraform-state-locks"
    }
  ]
}

State Contains Secrets

Remember: state files contain resource attributes in plain text. This includes:

  • Database passwords
  • API keys
  • Private keys
  • Connection strings

Treat state with the same security as your most sensitive secrets.

Quick Reference

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Initialize/reconfigure backend
terraform init -reconfigure

# Migrate between backends
terraform init -migrate-state

# List state
terraform state list

# Show resource
terraform state show RESOURCE

# Move resource
terraform state mv SOURCE DEST

# Remove from state
terraform state rm RESOURCE

# Import existing
terraform import RESOURCE ID

# Force unlock
terraform force-unlock LOCK_ID

Remote state is non-negotiable for any serious Terraform usage. Start with S3 + DynamoDB for AWS, enable versioning from day one, and never share state files outside your backend. The 30 minutes spent setting this up saves hours of state recovery later.