Import, Moved & Removed Blocks
By the end of this lesson you will be able to adopt existing resources into Terraform without recreating them, rename or reorganise resources without destroying them, and let go of resources Terraform should no longer manage. All three jobs use small blocks in your code, so every change shows up in the plan and can be reviewed.
Three ways state and code can drift apart
Real projects are messy. Three situations come up again and again:
- Something exists, but Terraform does not know about it. A bucket was made by hand in 2019. You want Terraform to manage it from now on. Use an
importblock. - Terraform knows it, but you renamed it in code.
aws_instance.serverbecameaws_instance.web. Without help, Terraform would destroy one and create another. Use amovedblock. - Terraform knows it, but should forget it. The database is moving to another team's configuration. Use a
removedblock.
Think of a school register in Rohtak. A new student who has been attending for weeks is added to the register (import). A student whose name was spelled wrong gets the entry corrected, not a new admission (moved). A student who changes school is struck off the register, but continues to exist (removed).
Import blocks
An import block (Terraform 1.5 and later) says: "the real object with this ID belongs at this address". Ishaan's team in Jaipur has a hand-made bucket for exam reports:
import {
to = aws_s3_bucket.reports
id = "jaipur-school-reports-2019"
}The id format depends on the resource type. For an S3 bucket it is the bucket name; for an EC2 instance it is the instance ID. Each resource's documentation page on the Terraform Registry has an "Import" section that shows the format.
Step 1: Let Terraform write the starting code
You also need a resource block that describes the bucket. Writing it by hand is slow, so Terraform can generate a first draft:
terraform plan -generate-config-out=generated.tf # aws_s3_bucket.reports will be imported
# (config will be generated)
resource "aws_s3_bucket" "reports" {
bucket = "jaipur-school-reports-2019"
id = "jaipur-school-reports-2019"
}
Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.The output is trimmed. Terraform wrote generated.tf with every argument it found, marked with a __generated__ comment at the top.
Step 2: Tidy the generated code
Open generated.tf. It often lists default values and sometimes conflicting arguments. Remove what you do not need, replace hard-coded values with variables where it helps, and move the block into your normal files.
Step 3: Plan and apply
Run terraform plan again. The goal is a plan that says 1 to import, 0 to add, 0 to change, 0 to destroy. If it shows changes, your code does not match the real bucket yet; adjust it until it does. Then terraform apply records the bucket in state. Nothing in AWS changes.
After a successful import, you may delete the import block or keep it as a record. Keeping it does no harm, because an import that is already done is ignored.
Importing many objects
Since Terraform 1.7, import blocks accept for_each. Here three hand-made buckets are adopted in one go:
locals {
old_buckets = {
hindi = "jaipur-notes-hindi"
science = "jaipur-notes-science"
maths = "jaipur-notes-maths"
}
}
import {
for_each = local.old_buckets
to = aws_s3_bucket.notes[each.key]
id = each.value
}
resource "aws_s3_bucket" "notes" {
for_each = local.old_buckets
bucket = each.value
}Moved blocks
Renaming a resource in code changes its address. Terraform sees "old address gone, new address added" and plans a destroy and a create. For a database, that is a disaster.
A moved block (Terraform 1.1 and later) tells Terraform that the object simply has a new address. Try it with the local provider. In the hello-tf folder, apply the configuration again so local_file.welcome exists. Then change its name to local_file.greeting and add:
moved {
from = local_file.welcome
to = local_file.greeting
}terraform plan # local_file.welcome has moved to local_file.greeting
resource "local_file" "greeting" {
filename = "./welcome.txt"
# (some unchanged attributes hidden)
}
Plan: 0 to add, 0 to change, 0 to destroy.The output is trimmed. Nothing is destroyed. After apply, the state uses the new address.
Moved blocks also handle bigger refactors:
- Moving a resource into a module:
from = aws_instance.web,to = module.web.aws_instance.this. - Switching from
counttofor_each:from = aws_instance.web[0],to = aws_instance.web["mumbai"]. - Renaming a module call:
from = module.old_name,to = module.new_name.
Keep moved blocks in the code for a while, at least until everyone who uses the configuration (including other environments) has applied. If you write a reusable module, keep them for a long time, because users may upgrade from old versions.
Removed blocks
Sometimes Terraform should stop managing an object without deleting it. Deleting the resource block alone would plan a destroy. Instead, delete the block and add a removed block (Terraform 1.7 and later):
removed {
from = aws_db_instance.legacy
lifecycle {
destroy = false
}
}The plan then says aws_db_instance.legacy will no longer be managed by Terraform. After apply, the database keeps running, but it is no longer in this state. Another configuration can now import it. With destroy = true, the block would instead destroy the object, which makes the intention explicit in review.
Common mistakes
- Applying an import plan that also shows changes. Your code does not match reality yet. Fix the code until the plan is import-only, unless you really want those changes.
- Renaming without a
movedblock. Always check the plan for-/+or-after any rename. - Deleting a resource block to "forget" it. That plans a destroy. Use a
removedblock withdestroy = false. - Removing moved blocks too early. Another environment that has not applied yet will then plan a destroy and create.
Key points
importblocks adopt existing objects into state;-generate-config-outwrites a first draft of the code.- A clean import plan says "1 to import" with nothing to add, change or destroy.
importsupportsfor_eachfrom Terraform 1.7.movedblocks rename or restructure resources and modules without destroying them.removedblocks withdestroy = falsemake Terraform forget an object while it keeps running.- All three are code, so they appear in the plan and go through review, unlike the old CLI commands.
Exercise
Your configuration has aws_instance.app[0] and aws_instance.app[1] created with count. You are switching to for_each with the keys "pune" and "kochi", in that order. Write the moved blocks so neither server is replaced.
Show solution
moved {
from = aws_instance.app[0]
to = aws_instance.app["pune"]
}
moved {
from = aws_instance.app[1]
to = aws_instance.app["kochi"]
}Plan: 0 to add, 0 to change, 0 to destroy.
Each index is mapped to its new key, so the plan only moves addresses in state. Both servers keep running untouched.
Quick Quiz
1. You renamed aws_instance.server to aws_instance.web. What stops Terraform from replacing it?
moved tells Terraform the object only changed its address.
2. What does a clean import plan look like?
Any other changes mean the code does not yet match the real object.
3. How do you make Terraform forget a database but keep it running?
Deleting the block alone would plan a destroy.
Learn Terraform & Ansible with a mentor
Free lessons get you started. Our Cloud & DevOps training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
