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
Comments (0)
This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!
ReplyLeave a Comment