skip to content

Import, Moved, and State Surgery

Adopting hand-built resources and refactoring module layouts both come down to editing the state mapping rather than the infrastructure. These commands are what separate people who have run Terraform in anger from people who have only read about it.

part ofTerraformoverview, primer and where to startread it →
on this pageshow

questions

6

In Terraform, what does importing a resource actually do to state and configuration, and how does the `import` block differ from the `terraform import` command?

level: middleimportance: must knowfreq 78%

answer

  1. state operation, not a creation
  2. the block must already exist — classic form
  3. 1.5 made it declarative
  4. -generate-config-out drafts, it does not finish
  5. empty plan is the acceptance test

basics

~20 s

Importing binds an already-existing object to a resource address in Terraform state. It creates no infrastructure and writes no configuration for you. The terraform import command needs the resource block written first; a Terraform 1.5+ import block runs inside plan and apply.

solid answer

~60 s

Import is purely a state operation: it tells Terraform "this address now corresponds to this real object", using the provider's ID format. It never creates anything, and — in the classic `terraform import ADDRESS ID` form — it never writes configuration either, so the resource block has to exist first or the command errors out. That made adoption a manual loop: write the block by hand, import, plan, fix the drift the plan reports, repeat. Since Terraform 1.5 you can instead put an `import` block in the configuration with `to` and `id`, which makes import part of the normal plan-and-apply flow: the plan shows exactly what will be imported and gets reviewed like any other change, and it is idempotent, so re-running it in CI is safe. Adding `terraform plan -generate-config-out=generated.tf` writes starter resource blocks for those imports. In both forms the acceptance test is identical — after importing, `terraform plan` must report no changes. If it still wants to modify or replace the resource, your configuration does not match reality yet, and applying would change production.

code

hcl · 8 lines
hcl
import {
  to = aws_s3_bucket.assets
  id = "my-existing-bucket"
}

resource "aws_s3_bucket" "assets" {
  bucket = "my-existing-bucket"
}

go deeper

for a junior

Be ready to say that import only records an existing object in state, creates nothing, and that the resource block must exist first when using the terraform import command.

for a middle

Explain the mechanics: provider-specific ID formats, the import block introduced in Terraform 1.5, -generate-config-out drafting starter configuration, and the empty plan as the proof that adoption succeeded.

for a senior

Show the judgment: adoption in reviewable slices, refusing to apply a post-import plan that proposes changes, and knowing which attributes a provider cannot read back and will therefore diff forever.

for a principal

Own the policy angle — adoption should arrive as pull requests whose plans show every object being taken over, not as terminal commands, so ownership of the estate is a reviewable, auditable event rather than tribal knowledge.

## What import actually does Terraform can only manage what its state maps. A resource address in your configuration with no state entry is, as far as Terraform is concerned, something that does not exist yet, and the plan will propose creating it. If the object *does* already exist — someone built it in the console, another tool made it, it predates your Terraform adoption — you get the classic brownfield failure: either a duplicate resource, or an outright error because the name is taken. Import is the fix, and it does exactly one thing: it records that a given resource address corresponds to a given existing object, identified by whatever ID string the provider expects. It calls the provider's read for that object and stores the resulting attributes. It creates nothing, destroys nothing, and modifies nothing in the cloud. ## The classic command ```bash terraform import aws_s3_bucket.assets my-existing-bucket ``` Two constraints matter. First, the resource block `resource "aws_s3_bucket" "assets" {}` must already exist in the configuration — otherwise Terraform errors with a message that the address does not exist in the configuration. Import binds config to reality; with no config there is nothing to bind. Second, the ID format is provider-specific and often not what you would guess: some resources want an ARN, some a bare name, some a composite of two IDs joined by a separator. The provider's documentation page for the resource states the import ID format, and getting it wrong either errors or, worse, imports the wrong object. The command mutates state directly, outside the plan-and-apply flow. Nothing about it is reviewable: it happened on someone's laptop, and the only record is the state's serial going up. ## The import block Terraform 1.5 introduced a declarative form: ```hcl import { to = aws_s3_bucket.assets id = "my-existing-bucket" } resource "aws_s3_bucket" "assets" { bucket = "my-existing-bucket" } ``` Now import is part of the configuration. `terraform plan` shows the import alongside any changes it would cause, so a pull request reviewer can see precisely which objects are being adopted before anything is applied. It runs in CI with the same credentials and the same lock as everything else, and it is idempotent: once the object is in state, re-planning with the block still present is a no-op. As of Terraform 1.7, an import block can take `for_each`, so a whole set of existing objects can be adopted in one declaration instead of a shell loop. Because the block is inert after the import has been applied, teams typically leave it in for a release and then delete it in a follow-up commit. ## Generating configuration The honest pain of adoption is writing configuration that matches an object you did not create. Terraform will draft it for you: ```bash terraform plan -generate-config-out=generated.tf ``` For every import block whose target resource block is missing, Terraform writes a resource block reflecting the object's current attributes into that file. The file must not already exist. Treat the output as a **starting point**, not a finished module: it is verbose, it hardcodes values that ought to be variables, it can emit attributes that conflict or that you would never write by hand, and it needs a human pass before it belongs in your repository. ## The acceptance test: an empty plan After import — either form — run a plan. The only acceptable result is no changes. Anything else means the configuration you wrote differs from the object as it really is, and applying would push your guesses into production. Common causes: tags you did not copy, a default the provider fills in that you left unset, a nested block the object has and your config does not, or an attribute the provider cannot read back at all (a password, some user data) which will show a perpetual diff you must handle deliberately. ## What import does not do It does not cascade. Importing an IAM role does not import its attached policies; importing a VPC does not import its subnets, route tables or gateways; where a provider models rules as separate resources, each one is its own import. Adoption is therefore an inventory exercise, not a single command. It also does not touch the object's identity in the cloud. If you import the wrong object, nothing breaks immediately — the damage arrives on the next apply, when Terraform reshapes the wrong resource to match your configuration. That is exactly why the reviewable, plan-visible import block is preferable to a command typed at a terminal.

  • After a successful import, the plan still wants to replace the resource. What went wrong and what do you do?
    Your configuration does not match the object. Some argument you wrote — often one that forces replacement, like a name or an availability zone — differs from reality. Read the plan's diff carefully, edit the configuration to match what exists, and re-plan until it is empty. Never apply a post-import plan that proposes a replacement.
  • Why can't `terraform import` write the configuration for you the way the import block can?
    It could not see a plan. The command mutates state directly and outside the plan graph, so there is no point at which Terraform is rendering proposed configuration. The import block runs inside plan, which is what made `-generate-config-out` possible: Terraform already has the read attributes in hand while producing plan output.
  • You need to adopt eighty existing objects of the same type. What is the cleanest approach?
    Use an import block with `for_each` over a map of address key to existing ID, which Terraform 1.7 and later supports, and generate the starter configuration in one pass. That keeps the whole adoption in one reviewable commit rather than eighty terminal commands whose only record is a bumped state serial.
  • Does importing a resource change anything in the cloud?
    No. Import performs a read through the provider and writes to state; the object itself is untouched. The risk is deferred: the very next apply reconciles that object against your configuration, so a wrong import or mismatched config is where real damage happens.

saying these in an interview costs you the question

  • Thinks terraform import creates the resource in the cloud
  • Believes import writes the matching resource block automatically
  • Applies a post-import plan that still proposes changes
  • Assumes importing a parent resource pulls in its children
  • Guesses the import ID format instead of checking the provider docs

context

open as a page

What does `terraform state rm` do to the real cloud object, and when is dropping a resource from Terraform state the right move rather than a mistake?

level: seniorimportance: must knowfreq 55%

basics

~20 s

terraform state rm deletes only the state entry: the cloud object keeps running, now managed by nobody. It is right when handing an object to another state or deliberately releasing it from Terraform; since Terraform 1.7 a removed block does the same thing declaratively.

open as a page

In Terraform, what do `terraform state list` and `terraform state show` tell you, and how do they differ from the other `terraform state` subcommands?

level: juniorimportance: should knowfreq 50%

basics

~10 s

terraform state list prints the resource addresses Terraform is tracking; terraform state show <address> prints that one resource's recorded attributes. Both only read state. The other subcommands, such as mv and rm, rewrite it.

open as a page

You rename a Terraform resource and move it inside a module, and the plan now shows a destroy and a create. How do you make the refactor without touching the real infrastructure?

level: middleimportance: should knowfreq 62%

basics

~20 s

Add a moved block with the old address in from and the new address in to. Terraform then rewrites the state mapping during plan and apply instead of destroying and recreating the object. The alternative, terraform state mv, does the same thing imperatively and unreviewably.

open as a page

You inherit a production environment that was built by hand in a cloud console, and you must bring it under Terraform without recreating anything. How do you approach it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Adopt in small slices ordered by blast radius, write or generate configuration for each slice, import it, and treat a plan reporting no changes as the acceptance test before moving on. Never apply a post-import plan that still proposes changes.

open as a page

As the author of a shared Terraform module, how do you restructure its internal resource addresses without forcing every consumer into a destroy and create?

level: principalimportance: nice to knowfreq 26%

basics

~20 s

Ship moved blocks inside the module itself. They travel with the module version, so each consumer's next plan re-keys their own state entries automatically. Keep them for a documented deprecation window and drop them only at a major version boundary.

open as a page