Migration #
As infrastructure and teams grow, the need to move state often arises. Migrating from local to remote backend when onboarding new team members, splitting an oversized monolithic state, or moving from one cloud provider to another — all involve state migration. A careless migration can cause orphaned resources or out-of-sync state. This article walks through the process step by step.
flowchart TD
A["🔄 Need State Migration?"] --> B{"Migration Type?"}
B --> C["Local → Remote"]
B --> D["Remote → Remote"]
B --> E["Monolithic → Split"]
C --> F["terraform init\n-migrate-state"]
D --> G["Pull backup →\nUpdate backend →\nInit migrate-state"]
E --> H["terraform state mv\n-state-out"]
style A fill:#3b82f6,stroke:#1e40af,color:#fff
style B fill:#f59e0b,stroke:#d97706,color:#fff
style C fill:#3b82f6,stroke:#1e40af,color:#fff
style D fill:#3b82f6,stroke:#1e40af,color:#fff
style E fill:#3b82f6,stroke:#1e40af,color:#fff
style F fill:#10b981,stroke:#059669,color:#fff
style G fill:#10b981,stroke:#059669,color:#fff
style H fill:#10b981,stroke:#059669,color:#fffMigrating from Local to Remote Backend #
This is the most common migration — a project that started with local state needs to move to S3 or another backend as the team grows.
LOCAL → S3 MIGRATION STEPS:
1. Create the S3 bucket and DynamoDB table (if they don't exist)
2. Add the backend configuration to the Terraform configuration
3. Run terraform init -migrate-state
4. Verify the state was successfully moved
5. Commit the backend configuration changes to Git
6. Delete the local terraform.tfstate (no longer needed)
# Step 2: Add the backend configuration to providers.tf or backend.tf
# BEFORE (no backend block — using local):
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
# AFTER (with a remote backend):
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "my-terraform-state"
key = "production/terraform.tfstate"
region = "ap-southeast-1"
dynamodb_table = "terraform-state-lock"
encrypt = true
}
}
# Step 3: Run init with the migrate-state flag
terraform init -migrate-state
# Output:
# Initializing the backend...
# Do you want to copy existing state to the new backend?
# Pre-existing state was found while migrating the previous "local" backend to
# the newly configured "s3" backend. An existing non-empty state already exists
# in the new backend. The two states have been merged. Please check the output
# above to ensure this is correct before continuing.
#
# Enter a value: yes
# Verify the state was successfully moved
terraform state list # Should show the same resources as before
# Step 6: Delete the local state (after confirming the remote state works)
rm terraform.tfstate terraform.tfstate.backup
Migrating Between Remote Backends #
Sometimes you need to move from one remote backend to another — for example, from Terraform Cloud to S3, or from S3 to GCS.
# Step 1: Pull the current state locally as a backup
terraform state pull > state-backup-$(date +%Y%m%d-%H%M%S).tfstate
# Step 2: Update the backend configuration to the new backend
# (edit backend.tf)
# Step 3: Run init with migrate-state
terraform init -migrate-state
# Step 4: Verify
terraform plan # Should be "No changes"
# Step 5: Store the state backup in a safe place
# Don't delete the backup until you're sure the migration succeeded
Splitting Monolithic State #
State that’s too large — hundreds of resources in one state file — becomes a problem because every apply has to process all resources at once, plans become slow, and the risk of an apply failing midway grows.
SCENARIO: Splitting a monolithic production state into networking + compute
State BEFORE (one file, all resources):
aws_vpc.main
aws_subnet.public[0]
aws_subnet.public[1]
aws_subnet.private[0]
aws_subnet.private[1]
aws_internet_gateway.main
aws_instance.web[0]
aws_instance.web[1]
aws_instance.worker[0]
aws_rds_instance.main
...
State AFTER (two separate files):
networking/terraform.tfstate:
aws_vpc.main
aws_subnet.public[0..1]
aws_subnet.private[0..1]
aws_internet_gateway.main
compute/terraform.tfstate:
aws_instance.web[0..1]
aws_instance.worker[0]
aws_rds_instance.main
flowchart TD
subgraph MONO["❌ Monolithic State"]
direction TB
M1["🗂️ All Resources\none state file"]
M2["aws_vpc.main"]
M3["aws_subnet.public"]
M4["aws_instance.web"]
M5["aws_rds_instance.main"]
M1 --- M2
M1 --- M3
M1 --- M4
M1 --- M5
end
subgraph SPLIT["✅ Separate States"]
direction LR
subgraph NET["Networking State"]
N1["aws_vpc.main"]
N2["aws_subnet.public"]
N3["aws_internet_gateway"]
end
subgraph COMP["Compute State"]
C1["aws_instance.web"]
C2["aws_rds_instance.main"]
end
end
MONO -->|"terraform state mv\n-state-out"| SPLIT
style MONO fill:#ffebee,stroke:#c62828
style SPLIT fill:#e8f5e9,stroke:#2e7d32
style NET fill:#e3f2fd,stroke:#1565c0
style COMP fill:#fff3e0,stroke:#e65100# HOW TO SPLIT STATE — using terraform state mv
# 1. Prepare a new directory for the compute workspace
mkdir -p environments/production/compute
cd environments/production/compute
# Create the Terraform configuration for compute resources
# (move the relevant resources from the monolithic configuration)
# 2. Initialize the new workspace with a different remote backend
terraform init
# 3. Move resources from the old state to the new state
# (run from the monolithic directory)
cd ../ # back to the monolithic directory
terraform state mv \
-state-out=compute/terraform.tfstate \
aws_instance.web[0] \
aws_instance.web[0]
# For many resources, use a script:
for i in 0 1; do
terraform state mv \
-state-out="../compute/terraform.tfstate" \
"aws_instance.web[$i]" \
"aws_instance.web[$i]"
done
# 4. Push the modified state to the remote backend
cd ../compute
terraform state push terraform.tfstate
# 5. Verify both workspaces
terraform state list # compute
cd ../networking
terraform state list # networking
Backup Before Migration #
# ALWAYS back up state before any migration
# Pull the current state
terraform state pull > pre-migration-backup.tfstate
# Verify the backup is readable
cat pre-migration-backup.tfstate | python3 -m json.tool > /dev/null
echo "Backup valid: $?"
# Store it somewhere safe (not just locally)
aws s3 cp pre-migration-backup.tfstate \
s3://my-backup-bucket/terraform-state-backups/$(date +%Y%m%d-%H%M%S).tfstate
Rollback If a Migration Goes Wrong #
# If the migration produced incorrect state:
# Option 1: Restore from backup
terraform state push pre-migration-backup.tfstate
# WARNING: This overwrites the current state with the backup
# Option 2: Revert the backend configuration
# - Remove or comment out the backend block from the configuration
# - Run terraform init -reconfigure
# - Terraform returns to local state or the previous backend
# Option 3: If using S3 with versioning enabled
# Restore the previous version from the S3 console or CLI
aws s3api list-object-versions \
--bucket my-terraform-state \
--prefix production/terraform.tfstate
aws s3api get-object \
--bucket my-terraform-state \
--key production/terraform.tfstate \
--version-id VERSION_ID \
restored-state.tfstate
Pre-Migration Checklist #
Before migrating state, prepare thoroughly to avoid data loss.
# STATE MIGRATION PRE-CHECKLIST:
# 1. Back up the current state
terraform state pull > backup-$(date +%Y%m%d-%H%M%S).tfstate
ls -la backup-*.tfstate # Verify the file exists and has a reasonable size
# 2. Make sure there are no pending changes
terraform plan
# Should be "No changes" — if not, apply first or resolve
# 3. Make sure there's no active lock
terraform state pull # If it succeeds, there's no lock
# 4. Verify nobody is currently working
# Check CI/CD pipelines, communicate with the team
# 5. Prepare a rollback plan
cp backup-*.tfstate emergency-rollback.tfstate
flowchart TD
A["Pre-Migration\nChecklist"] --> B["Back up state"]
B --> C["Verify plan\n= No changes"]
C --> D["Check no\nactive locks"]
D --> E["Coordinate\nwith the team"]
E --> F["Execute\nmigration"]
F --> G{"Success?"}
G -->|"Yes"| H["Verify:\nterraform plan = No changes"]
G -->|"No"| I["Rollback:\nterraform state push backup"]
style A fill:#e3f2fd,stroke:#1565c0
style H fill:#e8f5e9,stroke:#2e7d32
style I fill:#ffebee,stroke:#c62828Backend Migration #
Moving state from one backend to another is a risky operation.
# Scenario: Moving from local to S3
# 1. Configure the new backend (not yet initialized)
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "ap-southeast-1"
}
}
# 2. Run init — Terraform will detect the backend changed
terraform init
# Output: "Backend configuration changed. Do you want to copy existing state?"
# Answer: yes
# 3. Terraform automatically copies state from local to S3
# 4. Verify: terraform plan should be "No changes"
# ROLLBACK if there's a problem:
# 1. Change the backend config back to local
# 2. Run terraform init
# 3. Copy the state backup to the local directory
flowchart TD
A["State in\nLocal"] --> B["terraform init"]
B --> C{"Copy state\nto the new backend?"}
C -->|"Yes"| D["State copied\nto S3"]
C -->|"No"| E["State stays\nlocal"]
D --> F["Verify:\nterraform plan\n= No changes"]
F --> G{"OK?"}
G -->|"Yes"| H["Done ✅"]
G -->|"No"| I["Rollback ❌"]
style A fill:#fff3e0,stroke:#e65100
style D fill:#e8f5e9,stroke:#2e7d32
style H fill:#e8f5e9,stroke:#2e7d32
style I fill:#ffebee,stroke:#c62828State File Security #
# The state file contains ALL resource information:
# - IDs, ARNs, IP addresses
# - Passwords, API keys (if defined in Terraform)
# - Every attribute of every managed resource
# DON'T:
# - Commit state to Git
# - Store it on a local filesystem for production
# - Share state files without encryption
# MUST:
# - Store in an encrypted backend (S3+KMS, GCS, TFC)
# - Enable access logging
# - Restrict IAM access (only CI/CD roles)
Summary #
- Back up before any migration —
terraform state pull > backup.tfstateis the first step that must never be skipped.terraform init -migrate-stateto move between backends — Terraform will offer to copy the old state to the new backend.- Large monolithic state should be split into multiple workspaces for efficiency — use
terraform state mv -state-outto move resources between states.- Active S3 versioning is the best safety net — it allows restoring state to any version if something goes wrong.
- Verify after migration with
terraform plan— if the output is “No changes”, the migration succeeded and no resources were lost.- Don’t delete the old state until you’re sure the new state works correctly in all operations.