Skip to content

Terraform State Management — Remote State, Locking & Migration Guide

DodaTech Updated 2026-06-24 7 min read

In this tutorial, you'll learn about Terraform State Management. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Terraform state management is the practice of storing, locking, sharing, and migrating Terraform state files — the mapping between your configuration and real-world infrastructure — enabling team collaboration and preventing concurrent modification conflicts.

What You'll Learn

Why It Matters

Terraform state files contain sensitive information (resource IDs, IP addresses, passwords) and are the sole source of truth for your infrastructure. Losing state means losing the ability to manage existing resources. Sharing a single local <a href="/devops/terraform/">Terraform</a>.tfstate file via email or shared drives leads to corruption, conflicts, and data loss. DodaTech stores all Terraform state in AWS S3 with DynamoDB locking, enabling 20+ engineers to run <a href="/devops/terraform/">Terraform</a> plan and apply concurrently across 80+ workspaces.

Real-World Use

When DodaZIP's platform team runs <a href="/devops/terraform/">Terraform</a> apply, the S3 backend locks the state file via DynamoDB, preventing concurrent modifications. If an engineer's apply fails mid-way, the lock auto-expires after 20 minutes, and the team can diagnose the partial state rather than starting from scratch.

flowchart TD
    A[Terraform CLI] --> B[Backend Configuration]
    B --> C[S3 Bucket: dodatech-tfstate]
    B --> D[DynamoDB Table: tf-locks]
    C --> E[State File: env/production/network.tfstate]
    D --> F[Lock ID: 5a8b...]
    A --> G[terraform plan]
    G --> H[Read State from S3]
    H --> I[Acquire Lock via DynamoDB]
    I --> J[Compare with Configuration]
    J --> K[Generate Plan]
    K --> L[Release Lock]
    style C fill:#FF9900,color:#fff
    style D fill:#FF9900,color:#fff
â„šī¸ Info

Prerequisites: Familiarity with Terraform HCL syntax and basic AWS concepts (S3, DynamoDB).

Remote State Backend Configuration

# backend.tf
terraform {
  backend "s3" {
    bucket         = "dodatech-tfstate"
    key            = "env/production/network/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    kms_key_id     = "arn:aws:kms:us-east-1:123456789012:key/abc123"
    dynamodb_table = "terraform-state-locks"
    profile        = "dodatech-prod"
    # Optional: role_arn for cross-account access
    role_arn = "arn:aws:iam::123456789012:role/TerraformStateAccess"
  }
}
# Initialize backend (first time or after changing backend config)
terraform init

# Expected output:
# Initializing the backend...
# Do you want to copy existing state to the new backend?
#   Enter a value: yes
# Successfully configured the backend "s3"!
# Terraform will no longer write state to the local "terraform.tfstate" file.

Other Backend Types

# Azure Storage backend
terraform {
  backend "azurerm" {
    resource_group_name  = "dodatech-tfstate-rg"
    storage_account_name = "dodatetfstate"
    container_name       = "tfstate"
    key                  = "production/network.tfstate"
    access_key           = "storage-account-access-key"
  }
}

# Google Cloud Storage backend
terraform {
  backend "gcs" {
    bucket  = "dodatech-tfstate"
    prefix  = "terraform/production/network"
    project = "dodatech-platform"
  }
}

# Terraform Cloud backend
terraform {
  backend "remote" {
    organization = "dodatech"
    workspaces {
      name = "production-network"
    }
  }
}

State Locking with DynamoDB

# backend.tf — the DynamoDB table must exist before init
terraform {
  backend "s3" {
    bucket         = "dodatech-tfstate"
    key            = "env/production/network/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-state-locks"
  }
}
# Create the DynamoDB table
aws dynamodb create-table \
  --table-name terraform-state-locks \
  --attribute-definitions AttributeName=LockID,AttributeType=S \
  --key-schema AttributeName=LockID,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST \
  --region us-east-1

# If a lock prevents operation, force unlock (use cautiously)
terraform force-unlock LOCK_ID

# Check lock status
aws dynamodb get-item \
  --table-name terraform-state-locks \
  --key '{"LockID": {"S": "dodatech-tfstate/env/production/network/terraform.tfstate-md5"}}'

State Commands

# List resources in state
terraform state list

# Expected output:
# module.vpc.aws_vpc.this
# module.vpc.aws_subnet.public[0]
# module.vpc.aws_subnet.public[1]
# module.vpc.aws_subnet.private[0]
# module.vpc.aws_subnet.private[1]
# module.vpc.aws_internet_gateway.this

# Show details of a specific resource
terraform state show module.vpc.aws_vpc.this

# Expected output:
# # module.vpc.aws_vpc.this:
# resource "aws_vpc" "this" {
#     id                              = "vpc-0a1b2c3d4e5f67890"
#     arn                             = "arn:aws:ec2:us-east-1:123456789012:vpc/vpc-0a1b2c3d4e5f67890"
#     cidr_block                      = "10.0.0.0/16"
#     enable_dns_hostnames            = true
#     enable_dns_support              = true
#     tags                            = {
#         "Environment" = "production"
#         "Name"        = "dodatech-production-vpc"
#     }
# }

# Move a resource in state
terraform state mv \
  module.old_vpc.aws_vpc.this \
  module.vpc.aws_vpc.this

# Remove a resource from state (not destroy)
terraform state rm module.old_vpc.aws_vpc.this

# Import existing resource into state
terraform import aws_s3_bucket.state_bucket dodatech-tfstate

Importing Existing Infrastructure

# resources.tf
resource "aws_s3_bucket" "existing_logs" {
  # The bucket already exists in AWS
  bucket = "dodatech-production-logs"
}

resource "aws_s3_bucket_versioning" "existing_logs" {
  bucket = aws_s3_bucket.existing_logs.id
  versioning_configuration {
    status = "Enabled"
  }
}
# Import the existing bucket
terraform import aws_s3_bucket.existing_logs dodatech-production-logs

# Expected output:
# aws_s3_bucket.existing_logs: Importing from ID "dodatech-production-logs"...
# aws_s3_bucket.existing_logs: Import prepared!
# Import successful!
# The resources that were imported are shown above.

terraform plan

# Expected output:
# No changes. Your infrastructure matches the configuration.
# aws_s3_bucket_versioning.existing_logs will be created.

Workspaces for Environment Isolation

# Create workspaces
terraform workspace new dev
terraform workspace new staging
terraform workspace new production

# List workspaces
terraform workspace list

# Expected output:
#   default
#   dev
#   staging
# * production

# Switch workspace
terraform workspace select staging

# Use workspace in configuration
# backend.tf with workspace key
terraform {
  backend "s3" {
    bucket = "dodatech-tfstate"
    key    = "env/${terraform.workspace}/network/terraform.tfstate"
    region = "us-east-1"
    dynamodb_table = "terraform-state-locks"
  }
}
# variables.tf — vary values per workspace
variable "instance_type" {
  description = "EC2 instance type per environment"
  type        = string
}

locals {
  env_config = {
    dev      = { instance_type = "t3.medium", min_size = 1, max_size = 3 }
    staging  = { instance_type = "t3.large", min_size = 2, max_size = 6 }
    production = { instance_type = "m6i.xlarge", min_size = 5, max_size = 20 }
  }
  current_config = local.env_config[terraform.workspace]
}

State Migration

# Migrate from local to S3 backend
# Step 1: Update backend.tf with S3 config
# Step 2: Run init (Terraform prompts to copy state)
terraform init

# Migrate from S3 to Terraform Cloud
# Step 1: Update backend.tf with remote config
# Step 2: Run init
terraform init

# Split monolithic state into separate state files
# Step 1: Create new configuration directories
# Step 2: Use terraform state mv for each resource
terraform state mv \
  -state=./old-monolithic.tfstate \
  -state-out=./network/terraform.tfstate \
  module.network.aws_vpc.this module.vpc.aws_vpc.this

Common Configuration Mistakes

  1. Storing state locally in team projects: Local <a href="/devops/terraform/">Terraform</a>.tfstate is not shared, leading to conflicts when multiple engineers run Terraform. Always use a remote backend with locking for team projects.

  2. Using default workspace for production: Workspaces isolate environment state. Using the default workspace for production risks accidental destruction. Create dedicated workspaces per environment.

  3. Ignoring state file encryption: State files contain plaintext resource IDs and often secrets. Enable encrypt = true for S3 backends and use KMS customer-managed keys.

  4. Manual state manipulation without backups: Running <a href="/devops/terraform/">Terraform</a> state rm, <a href="/devops/terraform/">Terraform</a> state mv, or <a href="/devops/terraform/">Terraform</a> import without first backing up state is risky. Always copy the state file before manual operations.

  5. DynamoDB table not created before init: Terraform requires the DynamoDB table to exist before <a href="/devops/terraform/">Terraform</a> init configures the backend. Create it as a separate bootstrapping step.

Practice Questions

  1. Why is state locking important? Answer: State locking prevents concurrent <a href="/devops/terraform/">Terraform</a> apply operations from corrupting the state file. Only one Process holds the lock at a time.

  2. How do workspaces relate to state files? Answer: Each workspace produces a separate state file, isolating infrastructure changes for different environments (dev, staging, production) within the same configuration.

  3. What information is stored in a Terraform state file? Answer: State files contain resource metadata: IDs, attributes, dependencies, sensitive outputs, and the mapping between configuration addresses and real-world resource identifiers.

  4. When would you use <a href="/devops/terraform/">Terraform</a> import? Answer: Import existing infrastructure into Terraform management without destroying and recreating it. After import, write matching configuration to prevent drift.

Challenge

Set up a complete remote state infrastructure: create an S3 bucket with versioning and encryption enabled, create a DynamoDB table for state locking, configure a Terraform backend with workspace-based key prefixes, create workspaces for dev, staging, and production, migrate an existing local state file to the remote backend, import an existing AWS resource into state, and split a monolithic state into separate network and application state files.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro