- Authors
- Name
- Introduction
- What is Terraform State?
- Remote Backend Configuration
- Deep Dive into State Locking
- State Separation Strategies
- State Migration
- State Backup and Recovery
- Security Best Practices
- Conclusion

Introduction
When using Terraform alone, a local terraform.tfstate file is sufficient. However, in team environments, problems such as State conflicts, concurrent modifications, and State loss arise. This article covers how to safely manage Terraform State with practical examples.
What is Terraform State?
Terraform State is a JSON file that tracks the current state of your infrastructure.
# View the State file
cat terraform.tfstate | jq '.resources[0]'
{
"mode": "managed",
"type": "aws_instance",
"name": "web",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"attributes": {
"id": "i-0abc123def456789",
"ami": "ami-0c55b159cbfafe1f0",
"instance_type": "t3.medium"
}
}
]
}
Why State Matters
- Mapping: Maps Terraform code to actual resources
- Dependency tracking: Records dependencies between resources
- Performance: Minimizes API calls (checks State first)
- Collaboration: Shares infrastructure state among team members
Remote Backend Configuration
AWS S3 + DynamoDB
This is the most popular Remote Backend configuration:
# backend.tf - S3 Backend configuration
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "prod/networking/terraform.tfstate"
region = "ap-northeast-2"
encrypt = true
dynamodb_table = "terraform-state-lock"
# Additional security settings
kms_key_id = "alias/terraform-state"
}
}
Creating the S3 Bucket (Bootstrap)
# bootstrap/main.tf - State storage bootstrap
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-terraform-state-bucket"
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"
kms_master_key_id = aws_kms_key.terraform_state.arn
}
}
}
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
}
# DynamoDB for State Locking
resource "aws_dynamodb_table" "terraform_lock" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
# KMS Key for encryption
resource "aws_kms_key" "terraform_state" {
description = "KMS key for Terraform state encryption"
deletion_window_in_days = 30
enable_key_rotation = true
}
resource "aws_kms_alias" "terraform_state" {
name = "alias/terraform-state"
target_key_id = aws_kms_key.terraform_state.key_id
}
# Run the bootstrap (create with local State first)
cd bootstrap
terraform init
terraform apply
# The bootstrap's own State can also be migrated to S3 later
GCS (Google Cloud Storage)
terraform {
backend "gcs" {
bucket = "my-terraform-state"
prefix = "prod/networking"
# GCS natively supports State Locking
# No separate lock table like DynamoDB is needed
}
}
# Create a GCS bucket
gsutil mb -p my-project -l asia-northeast3 gs://my-terraform-state
gsutil versioning set on gs://my-terraform-state
Azure Blob Storage
terraform {
backend "azurerm" {
resource_group_name = "terraform-state-rg"
storage_account_name = "tfstate2026"
container_name = "tfstate"
key = "prod/networking/terraform.tfstate"
# Automatic locking via Azure Blob Lease
}
}
Deep Dive into State Locking
How DynamoDB Lock Works
# Lock acquisition process
# 1. terraform apply is executed
# 2. A LockID item is created in DynamoDB (PutItem with ConditionExpression)
# 3. If another user attempts apply, a lock conflict error occurs
# Check lock status
aws dynamodb get-item \
--table-name terraform-state-lock \
--key '{"LockID": {"S": "my-terraform-state-bucket/prod/networking/terraform.tfstate"}}' \
| jq '.Item.Info.S | fromjson'
When a Lock is Held
# Check lock information
terraform force-unlock <LOCK_ID>
# Caution: Only use force-unlock when a lock is genuinely stuck
# If another user is actively working, forcing the unlock can corrupt the State
Lock Timeout Configuration
# Set lock wait time (default 0s = fail immediately)
terraform apply -lock-timeout=5m
State Separation Strategies
Separation by Environment
terraform/
├── modules/
│ ├── networking/
│ ├── compute/
│ └── database/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ └── backend.tf # key = "dev/terraform.tfstate"
│ ├── staging/
│ │ ├── main.tf
│ │ └── backend.tf # key = "staging/terraform.tfstate"
│ └── prod/
│ ├── main.tf
│ └── backend.tf # key = "prod/terraform.tfstate"
Using Workspaces
# Create workspaces
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
# Switch workspace
terraform workspace select prod
# Check current workspace
terraform workspace show
# Branch configuration based on workspace
locals {
env = terraform.workspace
instance_type = {
dev = "t3.small"
staging = "t3.medium"
prod = "t3.large"
}
}
resource "aws_instance" "web" {
instance_type = local.instance_type[local.env]
# ...
}
DRY Management with Terragrunt
# terragrunt.hcl (root)
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
bucket = "my-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "ap-northeast-2"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
# environments/prod/networking/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../../modules/networking"
}
inputs = {
vpc_cidr = "10.0.0.0/16"
environment = "prod"
}
State Migration
Local to Remote
# 1. Add Remote Backend configuration to backend.tf
# 2. Run terraform init
terraform init
# Terraform will automatically suggest migration
# "Do you want to copy existing state to the new backend?"
# → Enter yes
Moving Resources Between States
# Remove a resource from State (actual infrastructure is preserved)
terraform state rm aws_instance.old_web
# Import a resource into another State
terraform import aws_instance.new_web i-0abc123def456789
# Move between States (Terraform 1.1+)
terraform state mv -state-out=../other/terraform.tfstate \
aws_instance.web aws_instance.web
The moved Block (Terraform 1.1+)
# Automatically update State when renaming resources
moved {
from = aws_instance.web
to = aws_instance.web_server
}
moved {
from = module.old_vpc
to = module.networking
}
State Backup and Recovery
Recovery via S3 Versioning
# List State file versions
aws s3api list-object-versions \
--bucket my-terraform-state \
--prefix prod/networking/terraform.tfstate \
| jq '.Versions[:5] | .[] | {VersionId, LastModified, Size}'
# Restore a specific version
aws s3api get-object \
--bucket my-terraform-state \
--key prod/networking/terraform.tfstate \
--version-id "abc123" \
terraform.tfstate.backup
# Push the restored State
terraform state push terraform.tfstate.backup
Automated State Backup
#!/bin/bash
# backup-state.sh
DATE=$(date +%Y%m%d-%H%M%S)
BUCKET="my-terraform-state-backup"
# Back up all State files
aws s3 sync s3://my-terraform-state/ s3://$BUCKET/$DATE/ \
--include "*.tfstate"
echo "Backup completed: $BUCKET/$DATE"
Security Best Practices
1. State File Encryption
# S3 SSE-KMS encryption (required)
backend "s3" {
encrypt = true
kms_key_id = "alias/terraform-state"
}
2. IAM Policy with Least Privilege
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::my-terraform-state/*"
},
{
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::my-terraform-state"
},
{
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem"],
"Resource": "arn:aws:dynamodb:*:*:table/terraform-state-lock"
}
]
}
3. Protecting Sensitive Values
# State files can contain sensitive data
output "db_password" {
value = aws_db_instance.main.password
sensitive = true
}
# Since values can still be viewed directly in terraform.tfstate,
# access control on the State file itself is critical
Conclusion
Terraform State management is the foundation of IaC. Key takeaways:
- Remote Backend is essential: Use S3/GCS + Locking for safe team collaboration
- Separate State: Separate State by environment/service to minimize blast radius
- Version control: Enable S3 Versioning for State recovery
- Security: Encryption + IAM least privilege + sensitive marking
Quiz (6 Questions)
Q1. What are the three main roles of Terraform State? Resource mapping, dependency tracking, and minimizing API calls (performance)
Q2. Which AWS service is used for State Locking? DynamoDB
Q3. Why does the GCS Backend not require a separate lock table? Because GCS natively supports Object Locking
Q4. When should terraform force-unlock be used? Only when a lock is abnormally stuck. It must not be used while another user is actively working
Q5. What is the purpose of the moved block? To automatically update the State when renaming resources, preventing resource recreation
Q6. What security measures are needed since State files can contain sensitive values? Encryption of the State file itself (SSE-KMS) and IAM access control