Back to Blog

Importing Existing Infrastructure into Terraform for the Terraform Associate Exam: terraform import, import Blocks, Drift Detection & moved Blocks

A practitioner's guide to bringing existing resources under Terraform management for the Terraform Associate (003/004) exam. Learn the terraform import command, declarative import blocks, refactoring with moved blocks, detecting configuration drift with -refresh-only, and how state ties it all together — with commands and real examples.

By Sailor Team , August 1, 2026

Terraform is wonderful when you start from an empty account and describe everything in code. Real life is messier: you inherit a production environment that was clicked together in a console, an emergency fix was made by hand, or a teammate created a resource outside the workflow. Bringing that existing infrastructure under Terraform’s control — without destroying and recreating it — is one of the most practical skills a Terraform practitioner needs, and it’s squarely on the Terraform Associate (003/004) exam.

This guide is written from a practitioner’s perspective. We’ll cover the two ways to import — the classic terraform import command and the newer declarative import blocks — plus how to detect and reconcile configuration drift, and how moved blocks let you refactor without recreating resources. Every one of these concepts is really a story about state, so if you want the foundation first, read the Terraform state management guide, then come back here. For the big picture, the Terraform Associate exam guide 2026 maps where import and state sit among the exam objectives.

Why Import Exists: State Is the Source of Truth

Terraform doesn’t manage the real world directly. It manages a state file — a JSON record of the resources it believes it owns — and reconciles that state against your configuration and the actual cloud API on every plan and apply. The core workflow (covered in init, plan, apply, destroy) assumes Terraform created every resource it knows about.

An existing resource that Terraform didn’t create isn’t in state. If you simply write configuration for it and run apply, Terraform will try to create a duplicate — or fail because the name already exists. Importing solves this by writing the real resource’s attributes into state and mapping it to a resource address in your configuration. After a successful import, Terraform treats that resource as if it had created it.

Two rules to remember:

  1. Import updates state, not your configuration. With the classic command, you still have to write the matching resource block yourself. Import does not generate HCL for you (the command never has; import blocks can, with a flag — more below).
  2. One import maps one real resource to one resource address. For resources created with count or for_each, you import each instance to its indexed address.

Method 1: The terraform import Command

The classic, imperative approach uses the CLI. The syntax is:

terraform import <RESOURCE_ADDRESS> <RESOURCE_ID>

The resource address is how Terraform names the resource in your config (for example aws_instance.web). The resource ID is provider-specific — an EC2 instance uses its instance ID, an S3 bucket uses its bucket name, an IAM role uses its role name. The provider documentation’s “Import” section tells you the exact ID format for every resource type.

A typical workflow to adopt an existing EC2 instance:

# 1. Write a placeholder resource block first
cat > main.tf <<'EOF'
resource "aws_instance" "web" {
  # attributes filled in after import
}
EOF

# 2. Import the real instance into that address
terraform import aws_instance.web i-0abc123def4567890

# 3. Inspect what Terraform now knows
terraform state show aws_instance.web

Step 3 is the key move: terraform state show prints every attribute Terraform recorded, and you copy the relevant ones into your resource block. Then run terraform plan and iterate until the plan shows “No changes.” A clean plan means your HCL now faithfully describes the imported resource. If the plan wants to change or replace the resource, your configuration doesn’t yet match reality — fix it before applying.

Importing indexed resources uses the full address, quoted to protect the brackets from the shell:

terraform import 'aws_instance.web[0]' i-0abc123def4567890
terraform import 'aws_instance.cluster["blue"]' i-0aaa111bbb2223334

(If you’re fuzzy on when Terraform uses numeric indexes versus string keys, the count vs. for_each guide explains the difference — it matters for import addresses.)

Method 2: Declarative import Blocks

Since Terraform 1.5, you can import declaratively with an import block in your configuration. This fits the Terraform philosophy far better: imports become part of your code, reviewable in a pull request, and executed as part of the normal plan/apply cycle rather than as a side-channel command.

import {
  to = aws_instance.web
  id = "i-0abc123def4567890"
}

resource "aws_instance" "web" {
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.micro"
  # ...
}

Run terraform plan and Terraform shows exactly what it will import and whether the configuration matches. Run terraform apply and the resource is imported. Import blocks have two big advantages over the command:

  • They can generate configuration for you. Run terraform plan -generate-config-out=generated.tf and Terraform writes a starter resource block for each import into generated.tf. You review and refine it instead of hand-writing every attribute — a huge time-saver when adopting dozens of resources.
  • They’re idempotent and safe to leave in place. After the import succeeds, the block is a no-op on future runs. Many teams remove import blocks after they’ve applied, but leaving them doesn’t cause re-imports.

Here’s the difference at a glance:

Aspectterraform import commandimport block
IntroducedEarly TerraformTerraform 1.5+
StyleImperative (CLI)Declarative (HCL)
Config generationNo (write HCL by hand)Yes (-generate-config-out)
Reviewable in VCSNoYes (it’s code)
Runs in plan/applyNo (separate command)Yes
Good forOne-off, scripted importsBulk, auditable adoption

For the exam, know that both exist, that the command only touches state while blocks are part of the config, and that -generate-config-out is the flag that produces starter HCL.

Configuration Drift: When Reality Diverges from State

Once resources are managed, the next real-world problem is drift — when the actual infrastructure changes outside Terraform. Someone edits a security group rule in the console, an autoscaling process changes a tag, or a manual hotfix bumps an instance type. Now the cloud no longer matches Terraform’s state, and your next apply may revert those changes or behave unexpectedly.

Terraform detects drift by refreshing — querying the provider APIs for the real state of managed resources and comparing it to the state file. Historically terraform refresh did this and wrote changes silently to state; that command is now deprecated in favor of safer, explicit options.

Detecting Drift with -refresh-only

The modern, safe way to see drift without changing anything is a refresh-only plan:

terraform plan -refresh-only

This compares real infrastructure to state and reports what has drifted without proposing to “fix” it against your configuration. If you want to update state to acknowledge the real-world values (accept the drift), apply it:

terraform apply -refresh-only

Contrast that with a normal terraform plan, which also refreshes but then proposes changes to make reality match your configuration — potentially undoing the out-of-band change. The distinction is exam-worthy:

  • terraform plan -refresh-only → “Tell me what changed outside Terraform” (reconcile state to reality).
  • terraform plan / apply → “Make reality match my code” (revert drift back to configuration).

You can also skip refreshing entirely for a faster, safer plan when you trust state (terraform plan -refresh=false), or target the reverse — re-create a resource that’s in a bad state — with -replace:

terraform apply -replace="aws_instance.web"

-replace is the modern successor to the deprecated terraform taint; it forces one resource to be destroyed and recreated on the next apply.

Refactoring Without Destroying: moved Blocks

A close cousin of import is the moved block, which handles a different problem: you’re already managing a resource, but you want to rename it or move it into a module without Terraform destroying and recreating it. Renaming a resource address normally makes Terraform think the old one is gone and a new one is needed — exactly what you don’t want in production.

A moved block tells Terraform that a resource simply changed address:

moved {
  from = aws_instance.web
  to   = aws_instance.web_server
}

resource "aws_instance" "web_server" {
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.micro"
}

On the next plan, Terraform updates state to track the resource under its new address — no destroy, no recreate. Moved blocks are ideal for refactors like renaming resources, extracting resources into a reusable module, or splitting a configuration. Like import blocks, they live in code and are reviewable.

Keep the three refactoring tools straight:

TaskTool
Adopt a resource Terraform doesn’t manageimport block / terraform import
Rename or relocate a managed resource in codemoved block
Force-recreate a managed resourceterraform apply -replace=...
Manually edit the mapping in stateterraform state mv / rm (advanced, use with care)

A Practical End-to-End Import Workflow

Putting it together, here’s a reliable sequence for adopting an existing environment:

  1. Inventory the resources you need to manage and find each provider-specific import ID.
  2. Write import blocks for them (or run terraform import if scripting).
  3. Run terraform plan -generate-config-out=generated.tf to scaffold configuration.
  4. Review and clean up the generated HCL — remove read-only attributes, parameterize with variables, organize into files.
  5. Run terraform plan repeatedly until it reports no changes.
  6. terraform apply to finalize imports into state.
  7. Commit everything, then use terraform plan -refresh-only on a schedule to catch future drift.

Store that state in a remote backend with locking (see the state management guide) or in HCP Terraform / Terraform Cloud, which adds automatic drift detection and run history on top of everything above.

How Import and Drift Show Up on the Exam

A few patterns to internalize before test day:

  • Import updates state, not configuration. The classic command never writes HCL; you must author the resource block. Only import blocks can generate config, via -generate-config-out.
  • A clean import ends in “No changes.” If a post-import plan wants to modify or replace the resource, the configuration doesn’t match reality yet.
  • -refresh-only reconciles state to reality; a normal plan reverts drift to your code. Know which direction each moves.
  • moved blocks prevent destroy/recreate during renames and module refactors.
  • -replace supersedes terraform taint for forcing recreation.
  • Import IDs are provider-specific — always check the resource’s “Import” documentation.

For a fast reference to all of these flags, keep the Terraform commands cheat sheet handy while you practice.

Frequently Asked Questions

Does terraform import generate configuration automatically?

No — the terraform import command only writes the resource’s current attributes into the state file. You still have to author the matching resource block in HCL yourself, typically by copying values from terraform state show. If you want Terraform to scaffold configuration for you, use a declarative import block with terraform plan -generate-config-out=<file>, which was added in Terraform 1.5.

What is the difference between the terraform import command and an import block?

The terraform import command is imperative: you run it from the CLI, it updates state only, and it isn’t part of your configuration or version control. An import block is declarative HCL you add to your configuration; it runs as part of the normal plan/apply cycle, is reviewable in a pull request, and can generate starter configuration with -generate-config-out. Blocks are preferred for bulk, auditable adoption; the command is handy for quick or scripted one-offs.

How do I detect configuration drift in Terraform?

Run terraform plan -refresh-only. This queries the real infrastructure and compares it to the state file, reporting anything that changed outside Terraform without proposing to revert it. To record those real-world values into state (accept the drift), run terraform apply -refresh-only. A regular terraform plan also refreshes, but it then proposes changes to make reality match your configuration — which would undo the out-of-band change.

What is a moved block and when should I use it?

A moved block tells Terraform that an already-managed resource simply changed its address — for example after you rename a resource or move it into a module. Without it, Terraform would see the old address disappear and the new one appear, and plan a destroy-and-recreate. The moved block updates state to track the resource under its new address instead, so no infrastructure is destroyed. Use it for renames, module extraction, and configuration refactors.

What replaced the terraform taint command?

Use terraform apply -replace="<resource_address>" (or terraform plan -replace=... to preview). It forces a single resource to be destroyed and recreated on the next apply, which is what terraform taint used to do. The taint command is deprecated in favor of -replace because -replace shows the effect in a normal plan before you commit to it.

Can I import resources created with count or for_each?

Yes. You import each instance to its full indexed address, quoting it so the shell doesn’t interpret the brackets — for example terraform import 'aws_instance.web[0]' i-0abc... for count, or terraform import 'aws_instance.web["blue"]' i-0abc... for for_each. With import blocks, set the to argument to the same indexed address. Each real resource maps to exactly one resource-instance address.

Conclusion and Next Steps

Bringing existing infrastructure under Terraform is fundamentally about state: import writes real resources into state so Terraform stops trying to recreate them, drift detection compares state to reality so you catch out-of-band changes, and moved blocks rewrite state addresses so refactors don’t destroy anything. Master the two import methods, know that -refresh-only reconciles state to reality while a normal plan reverts to your code, and remember that -replace has superseded taint — and you’ll handle the “adopt this messy environment” scenarios the Terraform Associate exam (and your job) throw at you.

The fastest way to turn these concepts into exam-day confidence is realistic practice. Sailor.sh’s HashiCorp Terraform Associate (004) Mock Exam Bundle gives you exam-style questions that mirror the real format and difficulty — including state, import, and drift scenarios like the ones above — with detailed explanations that surface the exact distinctions the exam tests. Working through realistic questions is the surest way to find your gaps before they cost you points.

Pair the practice with the Terraform Associate 30-day study plan, then reinforce the fundamentals with the state management guide, the core workflow walkthrough, and the commands cheat sheet.

Limited Time Offer: Get 80% off all Mock Exam Bundles | Sale ends in 7 days. Start learning today.

Claim Now