Stage 4 · Provision
Terraform State Operations
Import & Moved Blocks
terraform import, import blocks, moved blocks, and preserving addresses during refactors — restructuring without destroying.
terraform import Command
The terraform import command maps real-world resources to Terraform resource addresses. It does not write configuration — you must write the resource block first, then run import to associate it with the real resource.
$ terraform import aws_instance.web i-0123456789abcdef0
$ terraform import aws_vpc.main vpc-0123456789abcdef0
$ terraform import 'aws_security_group.web["allow_http"]' sg-0123456789abcdef0The import command takes the resource address and the real resource ID. For resources using for_each, include the key in quotes. After import, run terraform plan to verify the configuration matches.
$ terraform import module.vpc.aws_vpc.main vpc-0123456789abcdef0
$ terraform import 'module.ec2.aws_instance.web["web-1"]' i-0123456789abcdef0
$ terraform import module.rds.aws_db_instance.main rd-0123456789abcdef0Module imports use the full module path. The resource address must match the configuration. If the address does not exist, Terraform will error.
Import Blocks
import {
to = aws_instance.web
id = "i-0123456789abcdef0"
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "imported-web-server"
}
}Import blocks are declarative — they are evaluated during terraform plan. The resource block must exist with the correct configuration. Terraform verifies the imported resource matches the configuration.
import {
for_each = toset(["i-abc123", "i-def456"])
to = aws_instance.web[each.key]
id = each.value
}
resource "aws_instance" "web" {
for_each = toset(["i-abc123", "i-def456"])
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}The for_each import imports multiple resources in one declaration. Each key maps to a resource instance and a real resource ID. This is cleaner than running terraform import multiple times.
Moved Blocks
moved {
from = aws_instance.web_server
to = aws_instance.web
}
moved {
from = aws_security_group.old_name
to = aws_security_group.new_name
}Moved blocks tell Terraform to rename resources in state without destroying them. The old address is removed and the new address is added. No real infrastructure changes occur.
moved {
from = aws_instance.web
to = module.compute.aws_instance.web
}
moved {
from = aws_vpc.main
to = module.networking.aws_vpc.main
}
moved {
from = aws_subnet.private
to = module.networking.aws_subnet.private
}Moving resources into modules updates the state to reflect the new address. The resources are not recreated. This is the cleanest way to refactor from flat to modular configuration.
moved {
from = aws_instance.web["web-1"]
to = aws_instance.web["primary"]
}
moved {
from = aws_instance.web["web-2"]
to = aws_instance.web["secondary"]
}Moved blocks handle for_each key changes. The resource is renamed in state. This is essential when reorganizing resources with different naming conventions.
State Manipulation Commands
$ terraform state mv aws_instance.web aws_instance.web_server
$ terraform state mv 'aws_instance.web["a"]' 'aws_instance.web["primary"]'
$ terraform state mv module.old_name module.new_name
$ terraform state mv aws_instance.web module.compute.aws_instance.web
$ terraform state rm aws_instance.legacy
$ terraform state rm 'aws_instance.web["deprecated"]'State commands modify the state file directly. state mv renames or moves resources. state rm removes resources from state without destroying them. Always backup the state before running these commands.
Refactoring Patterns
$ terraform state pull > state-backup.json # Backup state
# Make code changes
$ terraform plan -migrate-state # Preview state changes
$ terraform apply # Apply the refactor
$ terraform state list # Verify all resourcesAlways backup before refactoring. The plan shows what state changes will occur. After applying, verify all resources are in the expected addresses.
Moved blocks are declarative and code-reviewable. State mv commands are imperative and not tracked in Git. Prefer moved blocks for refactoring — they are part of the codebase.
Import does not write configuration. You must write the resource block first. Without configuration, terraform plan will show errors because Terraform does not know what the resource should look like.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.