EthicsComputerCourses
Join training +91 90532 10052

Import, Moved & Removed Blocks

Terraform & Ansible · Lesson 12 of 29 Intermediate 7 min read Updated

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:

  1. 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 import block.
  2. Terraform knows it, but you renamed it in code. aws_instance.server became aws_instance.web. Without help, Terraform would destroy one and create another. Use a moved block.
  3. Terraform knows it, but should forget it. The database is moving to another team's configuration. Use a removed block.

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:

HCL (Terraform)
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:

Terminal
terraform plan -generate-config-out=generated.tf
Output
  # 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:

HCL (Terraform)
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:

HCL (Terraform)
moved {
  from = local_file.welcome
  to   = local_file.greeting
}
Terminal
terraform plan
Output
  # 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 count to for_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):

HCL (Terraform)
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 moved block. Always check the plan for -/+ or - after any rename.
  • Deleting a resource block to "forget" it. That plans a destroy. Use a removed block with destroy = false.
  • Removing moved blocks too early. Another environment that has not applied yet will then plan a destroy and create.

Key points

  • import blocks adopt existing objects into state; -generate-config-out writes a first draft of the code.
  • A clean import plan says "1 to import" with nothing to add, change or destroy.
  • import supports for_each from Terraform 1.7.
  • moved blocks rename or restructure resources and modules without destroying them.
  • removed blocks with destroy = false make 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
HCL (Terraform)
moved {
  from = aws_instance.app[0]
  to   = aws_instance.app["pune"]
}

moved {
  from = aws_instance.app[1]
  to   = aws_instance.app["kochi"]
}
Output
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?

2. What does a clean import plan look like?

3. How do you make Terraform forget a database but keep it running?

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