Terraform state is the source of truth for your infrastructure. It maps real-world resources to your configuration. Without proper state management, teams conflict, secrets leak, and infrastructure drifts. In this lesson, you'll learn remote backends, state locking, workspaces, and state migration—the practices that enable team collaboration at scale.

1. Learning Objectives

By the end of this lesson, you will be able to:

  • Configure remote state backends (S3, Azure, GCS)
  • Implement state locking with DynamoDB
  • Use Terraform workspaces for environment separation
  • Migrate local state to remote backends
  • Import existing resources into state
  • Understand state file structure and sensitive data

2. Why This Matters

Real-world scenario: Two engineers run `terraform apply` simultaneously on the same infrastructure. Without state locking, they corrupt the state file, causing resource conflicts and potential production outages. Remote backends with locking prevent this.

3. Core Concepts

AWS S3 Backend with DynamoDB Locking

# backend.tf - AWS S3 backend with state locking
terraform {
  backend "s3" {
    bucket         = "my-company-terraform-state"
    key            = "production/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
    kms_key_id     = "arn:aws:kms:us-east-1:123456789012:key/xxx"
  }
}

# Create DynamoDB table for locking (run once)
resource "aws_dynamodb_table" "terraform_lock" {
  name         = "terraform-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

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

  tags = {
    Name        = "Terraform State Lock Table"
    Environment = "Infrastructure"
  }
}

# Create S3 bucket (run once)
resource "aws_s3_bucket" "terraform_state" {
  bucket = "my-company-terraform-state"

  lifecycle {
    prevent_destroy = true
  }

  tags = {
    Name = "Terraform State Bucket"
  }
}

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 = "AES256"
    }
  }
}

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
}
AWS S3 backend with DynamoDB locking

Azure Backend

# backend.azurerm.tf
terraform {
  backend "azurerm" {
    resource_group_name  = "terraform-state-rg"
    storage_account_name = "terraformstate123"
    container_name       = "tfstate"
    key                  = "production.terraform.tfstate"
    access_key           = var.azure_storage_access_key
  }
}

# Create Azure storage account
resource "azurerm_resource_group" "terraform_state" {
  name     = "terraform-state-rg"
  location = "East US"
}

resource "azurerm_storage_account" "terraform_state" {
  name                     = "terraformstate123"
  resource_group_name      = azurerm_resource_group.terraform_state.name
  location                 = azurerm_resource_group.terraform_state.location
  account_tier             = "Standard"
  account_replication_type = "LRS"

  blob_properties {
    versioning_enabled = true
  }
}

resource "azurerm_storage_container" "terraform_state" {
  name                  = "tfstate"
  storage_account_name  = azurerm_storage_account.terraform_state.name
  container_access_type = "private"
}
Azure backend configuration

Google Cloud Backend

# backend.gcs.tf
terraform {
  backend "gcs" {
    bucket = "my-company-terraform-state"
    prefix = "production"
  }
}

# Create GCS bucket
resource "google_storage_bucket" "terraform_state" {
  name          = "my-company-terraform-state"
  location      = "US"
  force_destroy = false

  versioning {
    enabled = true
  }

  encryption {
    default_kms_key_name = "projects/my-project/locations/global/keyRings/terraform/cryptoKeys/state"
  }
}

resource "google_storage_bucket_iam_binding" "terraform_state" {
  bucket = google_storage_bucket.terraform_state.name
  role   = "roles/storage.objectAdmin"
  members = [
    "serviceAccount:terraform@my-project.iam.gserviceaccount.com",
  ]
}
Google Cloud backend configuration

Partial Backend Configuration

# backend.hcl - Partial configuration for different environments
# Production
bucket         = "my-company-terraform-state"
key            = "prod/terraform.tfstate"
region         = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt        = true

# Staging
# bucket         = "my-company-terraform-state"
# key            = "staging/terraform.tfstate"
# region         = "us-east-1"
# dynamodb_table = "terraform-locks"
# encrypt        = true

# Use with: terraform init -backend-config=prod.hcl

# Alternatively, use command line
# terraform init -backend-config="bucket=my-state-bucket" -backend-config="key=prod/terraform.tfstate"
Partial backend configuration

Terraform Workspaces

# Workspace commands
terraform workspace new dev
terraform workspace new staging
terraform workspace new production

# List workspaces
terraform workspace list

# Switch workspace
terraform workspace select dev

# Show current workspace
terraform workspace show

# Variable files per workspace
# dev.tfvars
# staging.tfvars
# production.tfvars

# In configuration, use workspace name
terraform {
  backend "s3" {
    bucket = "my-company-terraform-state"
    key    = "${terraform.workspace}/terraform.tfstate"  # Dynamic key
    region = "us-east-1"
  }
}

# Resource naming with workspace
resource "aws_instance" "web" {
  tags = {
    Name = "web-${terraform.workspace}"
    Environment = terraform.workspace
  }
}
Terraform workspaces for environment separation

State Commands

# List resources in state
terraform state list

# Show specific resource
terraform state show aws_instance.web

# Move resource in state
terraform state mv aws_instance.old aws_instance.new

# Remove resource from state (doesn't destroy)
terraform state rm aws_instance.to_remove

# Pull state to local file
terraform state pull > state.backup.json

# Push state (careful!)
terraform state push state.backup.json

# Replace a resource in state
terraform state replace-provider 'registry.terraform.io/-/aws' 'registry.terraform.io/hashicorp/aws'

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

# Import with specific ID
terraform import 'aws_security_group.web_sg' sg-12345678
Terraform state commands

4. Complete Project: Multi-Environment State Setup

# main.tf - Complete multi-environment setup
terraform {
  required_version = ">= 1.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  # Dynamic backend based on workspace
  backend "s3" {
    bucket         = "my-company-terraform-state"
    key            = "${terraform.workspace}/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

# Variables with workspace-specific defaults
variable "environment" {
  description = "Environment name"
  type        = string
  default     = "dev"
}

variable "instance_type" {
  description = "EC2 instance type"
  type        = map(string)
  default = {
    dev       = "t3.micro"
    staging   = "t3.small"
    production = "t3.medium"
  }
}

variable "instance_count" {
  description = "Number of instances"
  type        = map(number)
  default = {
    dev       = 1
    staging   = 2
    production = 3
  }
}

# Data source to get current workspace
locals {
  environment = terraform.workspace
  instance_type = var.instance_type[terraform.workspace]
  instance_count = var.instance_count[terraform.workspace]
}

# Provider configuration
provider "aws" {
  region = var.aws_region
  default_tags {
    tags = {
      Environment = local.environment
      ManagedBy   = "Terraform"
      Workspace   = terraform.workspace
    }
  }
}

# VPC (separate per environment)
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "vpc-${local.environment}"
  }
}

# Subnet
resource "aws_subnet" "main" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"

  tags = {
    Name = "subnet-${local.environment}"
  }
}

# Security group
resource "aws_security_group" "web" {
  name        = "web-sg-${local.environment}"
  description = "Web server security group"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.admin_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "web-sg-${local.environment}"
  }
}

# EC2 instances
resource "aws_instance" "web" {
  count = local.instance_count

  ami           = data.aws_ami.amazon_linux.id
  instance_type = local.instance_type
  subnet_id     = aws_subnet.main.id
  vpc_security_group_ids = [aws_security_group.web.id]

  user_data = <<-EOF
    #!/bin/bash
    echo "<h1>Hello from ${local.environment}</h1>" > /var/www/html/index.html
  EOF

  tags = {
    Name = "web-${local.environment}-${count.index + 1}"
  }
}

# Outputs for each environment
output "vpc_id" {
  value = aws_vpc.main.id
}

output "instance_ips" {
  value = aws_instance.web[*].public_ip
}

output "environment" {
  value = local.environment
}

# Deployment script
# ./deploy.sh
# #!/bin/bash
# export ENV=$1
#
# terraform workspace select $ENV || terraform workspace new $ENV
# terraform plan -var-file=$ENV.tfvars
# terraform apply -auto-approve -var-file=$ENV.tfvars
Complete multi-environment Terraform setup

State Migration

# Migrate from local to remote state

# 1. Ensure backend configuration is in place
# 2. Run init with -migrate-state
terraform init -migrate-state

# This copies local state to remote backend

# Migrate between backends (e.g., S3 to Azure)
# 1. Update backend configuration
# 2. Run init with -migrate-state
terraform init -migrate-state

# Migrate state between workspaces
terraform workspace new production
terraform state push -force /path/to/production.tfstate

# Split monolithic state into modules
# 1. Create new module configuration
# 2. Remove resources from old state
terraform state rm aws_vpc.main
# 3. Import into new module state
terraform import -chdir=./modules/vpc aws_vpc.main vpc-12345
State migration procedures

5. Common Errors & Solutions

6. Summary Checklist

7. Next Steps

Next lesson: Terraform Lesson 8.3: Terraform Modules

Gataya Med

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen February 18, 2025

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment