/Terraform Import, Moved Blocks & State Manipulation
Concept
Medium

Terraform Import, Moved Blocks & State Manipulation

13 min read·importmoved-blockstate-manipulationterraform-importstate-mvstate-rmreplacerefresh-onlyforce-unlockrefactoringterraform-associate

Importing existing infrastructure, safely renaming resources with moved blocks, and manipulating state directly are critical operational skills. The Terraform Associate exam tests the CLI import workflow, the config-driven import block (v1.5+), moved block patterns for refactoring, all terraform state subcommands, -replace vs taint, -refresh-only for drift detection, and force-unlock.


1. Why Import & State Manipulation?

Three real-world scenarios drive the need for these tools:

Rendering diagram…

2. terraform import — CLI Command

The original import mechanism. Writes to state but does not generate HCL — you must write the config manually first:

bash
1# Syntax
2terraform import <RESOURCE_ADDRESS> <REAL_WORLD_ID>
3
4# Common examples
5terraform import aws_instance.web          i-0abcd1234ef567890
6terraform import aws_s3_bucket.logs        my-company-access-logs
7terraform import aws_security_group.app    sg-0a1b2c3d4e5f67890
8terraform import aws_vpc.main              vpc-0123456789abcdef0
9terraform import aws_iam_role.lambda       my-lambda-execution-role
10terraform import aws_db_instance.postgres  prod-postgres-db
11terraform import aws_route53_record.www    Z1D633PJN98FT9_www.example.com_A
12
13# For_each resources — include the key in brackets
14terraform import 'aws_instance.servers["web"]'  i-0aaa111bbb222ccc
15terraform import 'aws_instance.servers["app"]'  i-0ddd333eee444fff
16
17# Module resources — include full module path
18terraform import module.compute.aws_instance.web  i-0abcd1234ef567890

The full CLI import workflow:

Rendering diagram…
hcl
1# Step 1: Write the resource block (minimal — just enough to import)
2resource "aws_instance" "web" {
3  # Leave empty or add a few known attributes
4  # Terraform will populate state from real infrastructure
5}
bash
1# Step 2: Run import
2terraform import aws_instance.web i-0abcd1234ef567890
3# Import successful. The resources that were imported are shown above.
4# These resources are now in your Terraform state and will henceforth
5# be managed by Terraform.
6
7# Step 3: See what config changes are needed
8terraform plan
9# Will show all attributes that differ between config and state
10# Add each one to your resource block until plan shows "No changes."

Finding the real-world ID to use with import:

Resource TypeID FormatHow to Find
aws_instancei-0abcd1234EC2 console → Instance ID
aws_s3_bucketBucket nameS3 console → Bucket name
aws_security_groupsg-0abc123EC2 → Security Groups
aws_vpcvpc-0abc123VPC console
aws_iam_roleRole name (not ARN)IAM console → Role name
aws_route53_recordZONE_ID_NAME_TYPERoute53 + record details
aws_db_instanceDB identifierRDS console → DB identifier

3. import Block — Config-Driven Import (Terraform 1.5+)

The modern approach: declare imports as code inside .tf files. Reviewable, repeatable, works in CI/CD:

hcl
1# import.tf — declare all imports here
2
3import {
4  to = aws_instance.web
5  id = "i-0abcd1234ef567890"
6}
7
8import {
9  to = aws_s3_bucket.logs
10  id = "my-company-access-logs"
11}
12
13import {
14  to = aws_security_group.app
15  id = "sg-0a1b2c3d4e5f67890"
16}
17
18# Import into a module resource
19import {
20  to = module.network.aws_vpc.main
21  id = "vpc-0123456789abcdef0"
22}
hcl
1# The corresponding resource blocks (must exist)
2resource "aws_instance" "web" {
3  ami           = "ami-0c55b159cbfafe1f0"
4  instance_type = "t3.micro"
5  # other attributes...
6}

Auto-generate config with -generate-config-out (Terraform 1.5+):

bash
1# Write import blocks, then let Terraform generate the resource HCL
2terraform plan -generate-config-out=generated_resources.tf
3
4# Terraform writes a complete resource block to generated_resources.tf
5# with all attributes filled in from the real infrastructure
6
7# Review and clean up generated_resources.tf, then apply
8terraform apply
hcl
1# Example of what terraform generates in generated_resources.tf:
2resource "aws_instance" "web" {
3  ami                         = "ami-0c55b159cbfafe1f0"
4  instance_type               = "t3.micro"
5  availability_zone           = "us-east-1a"
6  subnet_id                   = "subnet-0abc123"
7  vpc_security_group_ids      = ["sg-0abc123"]
8  key_name                    = "my-key-pair"
9  associate_public_ip_address = true
10  # ... many more attributes
11}

import block with for_each (Terraform 1.7+):

hcl
1# Import multiple resources at once using for_each
2locals {
3  servers_to_import = {
4    web = "i-0aaa111bbb222ccc3"
5    app = "i-0ddd333eee444fff5"
6    db  = "i-0ggg555hhh666iii7"
7  }
8}
9
10import {
11  for_each = local.servers_to_import
12  to       = aws_instance.servers[each.key]
13  id       = each.value
14}
15
16resource "aws_instance" "servers" {
17  for_each      = local.servers_to_import
18  ami           = data.aws_ami.ubuntu.id
19  instance_type = "t3.micro"
20}

CLI import vs import block:

Aspectterraform import CLIimport {} block
VersionAll versionsTerraform >= 1.5
Code reviewable?NoYes
Repeatable?Manual each timeRuns with terraform apply
Generates HCL?NoYes (with -generate-config-out)
For_each support?One at a timeYes (>= 1.7)
CI/CD friendly?NoYes
Preferred approach?LegacyModern standard

4. moved Block — Rename Without Destroy

When you rename a resource or move it into/out of a module, Terraform sees a new address and plans to destroy the old + create the new. The moved block tells Terraform these are the same resource:

hcl
1# Scenario 1: Simple resource rename
2# BEFORE: resource "aws_instance" "web_server"
3# AFTER:  resource "aws_instance" "app_server"
4
5moved {
6  from = aws_instance.web_server
7  to   = aws_instance.app_server
8}
9
10resource "aws_instance" "app_server" {
11  ami           = data.aws_ami.ubuntu.id
12  instance_type = "t3.micro"
13}
hcl
1# Scenario 2: Moving a resource into a module
2# BEFORE: resource "aws_instance" "web" (in root)
3# AFTER:  module "compute" → aws_instance.web
4
5moved {
6  from = aws_instance.web
7  to   = module.compute.aws_instance.web
8}
hcl
1# Scenario 3: Moving out of a module (module refactor)
2moved {
3  from = module.old_vpc.aws_vpc.main
4  to   = aws_vpc.main
5}
hcl
1# Scenario 4: Renaming a module itself
2moved {
3  from = module.server
4  to   = module.web_server
5}
hcl
1# Scenario 5: Moving indexed (count) to for_each
2# BEFORE: aws_instance.web[0], web[1], web[2]
3# AFTER:  aws_instance.web["frontend"], ["backend"], ["worker"]
4moved {
5  from = aws_instance.web[0]
6  to   = aws_instance.web["frontend"]
7}
8moved {
9  from = aws_instance.web[1]
10  to   = aws_instance.web["backend"]
11}
12moved {
13  from = aws_instance.web[2]
14  to   = aws_instance.web["worker"]
15}

moved block rules:

  • No real infrastructure changes — only state is updated
  • terraform plan shows the move: aws_instance.web_server has moved to aws_instance.app_server
  • After terraform apply, the moved block can be removed from config
  • Keep moved blocks temporarily to allow teammates to apply cleanly before removing
  • Can be declared in any .tf file — moved.tf by convention

moved block vs terraform state mv:

Aspectmoved {} blockterraform state mv
In version controlYesNo
Code reviewedYesNo
AuditableYes — git historyNo
UndoableYes — remove the blockManual reverse command
Works in CI/CDYesRequires manual intervention
Preferred approachYes (modern)Legacy

5. All terraform state Subcommands

bash
1# ── READ OPERATIONS (safe to run anytime) ──────────────────────────
2
3# List all tracked resources
4terraform state list
5
6# Filter by type, name, or module
7terraform state list aws_instance.web
8terraform state list 'module.vpc.*'
9terraform state list 'aws_instance.*'
10
11# Show full attributes of a resource (useful after import)
12terraform state show aws_instance.web
13terraform state show 'module.compute.aws_instance.web'
14
15# Download remote state as JSON (ALWAYS do this before any write ops)
16terraform state pull
17terraform state pull > backup_$(date +%Y%m%d_%H%M%S).tfstate
18
19
20# ── WRITE OPERATIONS (destructive — back up first!) ────────────────
21
22# Rename/move a resource in state (legacy — prefer moved block)
23terraform state mv aws_instance.web aws_instance.app_server
24terraform state mv aws_instance.web 'module.compute.aws_instance.web'
25terraform state mv 'module.old' 'module.new'
26
27# Remove a resource from state WITHOUT destroying real infra
28# Use when: stopping management, resource deleted out-of-band
29terraform state rm aws_instance.web
30terraform state rm 'module.legacy.aws_s3_bucket.data'
31terraform state rm 'aws_instance.servers["web"]'
32
33# Upload local state to remote backend
34# WARNING: overwrites remote — only use for emergency recovery
35terraform state pull > emergency_backup.tfstate
36terraform state push recovered.tfstate

terraform state rm use cases:

ScenarioCommand
Stop managing a resource (keep it running)terraform state rm aws_instance.legacy
Resource was manually deleted (fix plan errors)terraform state rm aws_s3_bucket.deleted
Moving resource to another Terraform configstate rm in source, then import in destination
Emergency: remove corrupted resource from statestate rm then re-import

6. Replacing Resources — -replace and taint

Force Terraform to destroy and recreate a specific resource on the next apply:

bash
1# ── MODERN: -replace flag (Terraform 0.15.2+) ──────────────────────
2
3# Preview what will be replaced
4terraform plan -replace="aws_instance.web"
5
6# Apply the replacement
7terraform apply -replace="aws_instance.web"
8
9# Multiple resources at once
10terraform apply -replace="aws_instance.web" -replace="aws_lb.frontend"
11
12
13# ── LEGACY: terraform taint (deprecated in 0.15.2) ─────────────────
14
15# Old way — marks resource as tainted in state
16terraform taint aws_instance.web
17terraform apply   # tainted resources are destroyed+recreated
18
19# Remove a taint mark without replacing
20terraform untaint aws_instance.web

When to use -replace:

  • Instance is misbehaving but config is correct
  • AMI has been updated and you want a fresh instance
  • Corrupted application state on the instance
  • Force certificate rotation on a TLS resource

Automatic replacement via lifecycle:

hcl
1resource "aws_instance" "web" {
2  ami           = data.aws_ami.ubuntu.id
3  instance_type = "t3.micro"
4
5  lifecycle {
6    # Replace whenever the launch template version changes
7    replace_triggered_by = [aws_launch_template.web.latest_version]
8  }
9}

7. Drift Detection — refresh-only

Drift occurs when real infrastructure changes outside Terraform (manual console edits, other tools). The -refresh-only flag detects and optionally accepts this drift:

bash
1# Detect drift — show what changed in real infra vs state
2# Does NOT modify state or real infra — safe to run anytime
3terraform plan -refresh-only
4
5# Example output when drift detected:
6# ~ aws_instance.web
7#   ~ tags = {
8#       + "LastModified" = "2024-01-15"   # someone added a tag manually
9#     }
10
11# Accept drift — update state to match real infra
12# Does NOT apply your config changes
13terraform apply -refresh-only
Rendering diagram…

Drift workflow options:

bash
1# Option A: Accept drift (real infra is correct, update state)
2terraform apply -refresh-only
3
4# Option B: Reject drift (revert real infra to match config)
5terraform apply    # standard apply overwrites manual changes
6
7# Option C: Investigate without changing anything
8terraform plan -refresh-only    # read-only drift report
CommandUpdates State?Updates Real Infra?
terraform plan -refresh-onlyNoNo
terraform apply -refresh-onlyYesNo
terraform planNoNo
terraform applyYesYes
terraform refresh (deprecated)YesNo

8. force-unlock — Breaking a Stuck Lock

If a plan or apply is interrupted, the state lock may not be released:

bash
1# Error you might see:
2# Error: Error acquiring the state lock
3# Lock Info:
4#   ID:        abc12345-...
5#   Operation: OperationTypeApply
6#   Who:       engineer@example.com
7#   Created:   2024-01-15 10:23:45 UTC
8
9# Release the lock manually
10terraform force-unlock abc12345-...
11
12# Force unlock bypassing confirmation prompt
13terraform force-unlock -force abc12345-...

Warning: Only use force-unlock when you are certain the original process is dead. If the original terraform apply is still running and you force-unlock, both processes will modify state simultaneously — causing corruption. Always verify the original process is dead before unlocking.


9. Complete Import Workflow — Real Example

Scenario: An S3 bucket, IAM role, and EC2 instance were created manually. Bring them all under Terraform management:

hcl
1# Step 1: Write import blocks (import.tf)
2import {
3  to = aws_s3_bucket.app_data
4  id = "prod-app-data-bucket-2024"
5}
6
7import {
8  to = aws_iam_role.app_role
9  id = "prod-app-execution-role"
10}
11
12import {
13  to = aws_instance.app
14  id = "i-0abcd1234ef567890"
15}
bash
1# Step 2: Generate HCL config for all three resources
2terraform plan -generate-config-out=imported_resources.tf
3# Creates imported_resources.tf with full resource definitions
4
5# Step 3: Review and clean up generated config
6# Remove read-only attributes (id, arn, etc.)
7# Remove attributes you don't want to manage
8# Fix any Terraform-specific formatting issues
9
10# Step 4: Validate the import will work cleanly
11terraform plan
12# Should show: Plan: 3 to import, 0 to add, 0 to change, 0 to destroy
13
14# Step 5: Apply the import
15terraform apply
16# Imports resources into state without modifying real infrastructure
17
18# Step 6: Remove import blocks (they've served their purpose)
19# Delete the contents of import.tf
20
21# Step 7: Verify
22terraform plan
23# Should show: No changes. Your infrastructure matches the configuration.

10. Complete Refactoring Workflow — Moving Into a Module

Scenario: Refactor a VPC resource from the root module into a network child module:

bash
1# Before refactoring — state has:
2# aws_vpc.main
3# aws_subnet.public[0]
4# aws_subnet.public[1]
5# aws_internet_gateway.main
hcl
1# Step 1: Create the module (modules/network/main.tf)
2resource "aws_vpc" "main" { ... }
3resource "aws_subnet" "public" { count = 2 ... }
4resource "aws_internet_gateway" "main" { ... }
5
6# Step 2: Add moved blocks to root main.tf
7moved {
8  from = aws_vpc.main
9  to   = module.network.aws_vpc.main
10}
11
12moved {
13  from = aws_subnet.public[0]
14  to   = module.network.aws_subnet.public[0]
15}
16
17moved {
18  from = aws_subnet.public[1]
19  to   = module.network.aws_subnet.public[1]
20}
21
22moved {
23  from = aws_internet_gateway.main
24  to   = module.network.aws_internet_gateway.main
25}
26
27# Step 3: Call the module in root main.tf
28module "network" {
29  source     = "./modules/network"
30  vpc_cidr   = var.vpc_cidr
31  # ...
32}
bash
1# Step 4: Plan — should show only moves, no destroy/create
2terraform plan
3# Output:
4# Terraform will perform the following actions:
5#   aws_vpc.main has moved to module.network.aws_vpc.main
6#   aws_subnet.public[0] has moved to module.network.aws_subnet.public[0]
7#   ...
8# Plan: 0 to add, 0 to change, 0 to destroy.
9# (4 moves)
10
11# Step 5: Apply — only state is updated, real infra unchanged
12terraform apply
13
14# Step 6: Remove moved blocks after team has applied

11. Quick Reference

OperationCommand / BlockNotes
Import (legacy)terraform import ADDR IDCLI only; no HCL generated
Import (modern)import { to = ... id = ... }Config-driven; Terraform >= 1.5
Generate HCL on importterraform plan -generate-config-out=file.tfTerraform >= 1.5
Bulk importimport { for_each = ... }Terraform >= 1.7
Rename without destroymoved { from = ... to = ... }No infra change; state only
Move into modulemoved { from = resource to = module.x.resource }
List stateterraform state listRead-only; safe
Inspect resourceterraform state show ADDRRead-only; safe
Rename in state (legacy)terraform state mv SRC DSTPrefer moved block
Remove from stateterraform state rm ADDRDoes NOT destroy real infra
Backup stateterraform state pull > backup.tfstateAlways do before write ops
Force replaceterraform apply -replace=ADDRDestroy + recreate
Taint (deprecated)terraform taint ADDRUse -replace instead
Accept driftterraform apply -refresh-onlyUpdates state only
Detect driftterraform plan -refresh-onlyRead-only diff
Release stuck lockterraform force-unlock LOCK_IDVerify original process is dead first