12.1 Import Existing Infrastructure

Key Takeaways

  • An import block (Terraform 1.5+) with to plus id is planned as an import, then apply binds the existing object into state without creating it
  • Terraform 1.12 adds an identity argument on import blocks; it is mutually exclusive with id and uses provider-defined keys
  • Legacy terraform import ADDRESS ID writes state immediately and does not generate configuration
  • After import, the resource block must match the live object or the next plan will change that object
  • terraform plan -generate-config-out writes an experimental best-guess HCL template for import targets that lack a resource block; review it before apply
Last updated: August 2026

12.1 Import Existing Infrastructure

Quick Answer: Write a resource block for the already-existing object. Add an import block whose to address matches that resource and whose id (or, on Terraform 1.12, identity) names the live object. Run terraform plan, fix any proposed updates until the plan is import-only, then terraform apply. The legacy terraform import ADDRESS ID command still works: it writes state immediately and does not generate configuration.

Objective 7a on Terraform Associate (004) is: import existing infrastructure into your Terraform workspace on Terraform 1.12. HashiCorp's current language docs treat the import block as the reviewable, pipeline-friendly path. The CLI command remains valid and is still tested as the older one-shot path.

Official references: Import resources overview, import block reference, Import a single resource, Generating configuration, and terraform import.

Why this objective appears on 004

Most brownfield incidents are import mistakes, not missing resource syntax. Someone runs terraform import with no matching HCL and the next plan destroys or recreates the object. Someone imports the same EC2 instance to two addresses. Someone treats -generate-config-out as a finished module. 7a checks that you can bind an already-created object into state, keep configuration aligned, and not invent a create/destroy.

What import actually does

Import does not create the remote object. The bucket, instance, or zone already exists. Terraform asks the provider to read that object, then records it in state at a resource address you choose. From that moment the object is managed: later plans compare configuration to that state and to the live API.

HashiCorp's rule: each remote object should be bound to one resource address. Importing the same object twice (two addresses, or two workspaces fighting over one ID) produces unwanted behavior because Terraform assumes it created every object it tracks.

Not every resource type is importable. The provider must implement import. The Registry page for that resource documents the ID format and whether import is supported. If import is missing, that is a provider limitation, not a missing Terraform CLI flag.

Modern path: the import block (Terraform 1.5+, current on 1.12)

Add an import block anywhere in the root module. HashiCorp recommends either an imports.tf file or placing each block next to its destination resource block.

import {
  to = aws_s3_bucket.logs
  id = "app-logs-004-example"
}

resource "aws_s3_bucket" "logs" {
  bucket = "app-logs-004-example"
}

Arguments you must know for 004:

ArgumentRequired?Role on Terraform 1.12
toYesDestination address (TYPE.LABEL, plus an index if count/for_each, or module.NAME....)
idOne of id or identityProvider-specific import ID; must be known at plan time
identityOne of id or identityNew in 1.12.0. Object of provider-defined keys that uniquely identify the resource. Mutually exclusive with id
for_eachNoImport many similar objects from a map or set
providerNoNon-default provider configuration (alias) used for the import read

The destination resource block must exist (unless you are using experimental config generation — below). Its type and label form the state address. You do not have to copy every attribute the provider stores in state, but you do have to set the arguments that differ from provider defaults. HashiCorp is explicit: omitted arguments take defaults; if the live object is not at those defaults, the next plan updates the object.

Terraform 1.12 identity (do not overclaim)

The 1.12.0 changelog (14 May 2025) adds identity on import blocks. It is an object, not a string, and it cannot appear in the same block as id. Keys and values come from that resource's provider documentation, not from a universal Terraform schema. Some types still import only with id (classic example: many compute instances use a single provider ID such as i-abcd1234). Some types can be identified by a set of attributes instead of that string. On the exam, pick identity only when the question says the provider documents an identity object. Do not invent keys.

import {
  to       = aws_s3_bucket.logs
  identity = {
    # keys are provider-defined — read the resource's import docs
  }
}

Plan, then apply

terraform plan
terraform apply

plan prints something like Plan: 1 to import, 0 to add, 0 to change, 0 to destroy when configuration already matches. If you also see changes, edit the resource block until those updates disappear (or until the remaining updates are ones you actually want). Then apply records the import. Terraform notes that it imported the object; it did not create it.

Import blocks are idempotent. After the object is in state at that address, another plan does not import it again. You may delete the import block or leave it as a historical record. HashiCorp documents both; leaving it is useful for later maintainers.

Addresses for indexed resources:

import {
  to = aws_instance.web[0]
  id = "i-abcd1234"
}

import {
  to = aws_instance.web["env"]
  id = "i-abcd1234"
}

import {
  to = module.instances.aws_instance.example
  id = "i-abcd1234"
}

for_each on the import block can walk a map of names to IDs and write to = aws_s3_bucket.this[each.key] with id = each.value. The destination resource block needs a matching for_each.

flowchart TD
    Live["Existing remote object"] --> Res["Write resource block"]
    Res --> Imp["Add import block: to + id or identity"]
    Imp --> Plan["terraform plan"]
    Plan --> Match{"Plan is import-only?"}
    Match -->|No: updates/destroys| Edit["Edit resource arguments"]
    Edit --> Plan
    Match -->|Yes| Apply["terraform apply"]
    Apply --> State["Object bound in state; now managed"]
    CLI["Legacy: terraform import ADDRESS ID"] --> Immediate["State written now; no HCL generated"]
    Immediate --> Align["Write matching config or next plan changes the object"]

Legacy path: terraform import ADDRESS ID

Usage: terraform import [options] ADDRESS ID.

terraform import aws_instance.foo i-abcd1234
terraform import module.foo.aws_instance.bar i-abcd1234
terraform import 'aws_instance.baz[0]' i-abcd1234
terraform import 'aws_instance.baz["example"]' i-abcd1234

Before this command you must write the resource block. The command finds the remote object from ID and writes it into state at ADDRESS. Official docs: importing via the CLI does not generate configuration. If you want generated HCL, use an import block plus -generate-config-out, not this command.

The CLI path is immediate. There is no separate apply of an import action — the state file (local or remote) is updated when the command succeeds. That is why teams prefer the block: reviewers see 1 to import on a normal plan. HashiCorp also notes that on HCP Terraform the import command runs locally, so it does not automatically see remote workspace variables. Set equivalent local variables or use the import block inside a remote run instead.

Useful contrast for 004:

import block + plan/applyterraform import ADDRESS ID
When it landedTerraform 1.5 languageLong-standing CLI
Writes stateOn apply after a reviewed planImmediately on success
Generates HCL?Only if you add experimental -generate-config-outNever
Many resourcesMultiple blocks or for_eachOne address per invocation
CI / reviewSame workflow as other changesOne-shot, easy to skip review
HCP TerraformImport action can run in the remote plan/applyCommand runs on the laptop

Configuration generation — what 1.12 actually supports

Do not claim that import writes a complete production module by itself. Official 1.12 support is this experimental feature (present since 1.5, still labeled experimental in HashiCorp docs):

terraform plan -generate-config-out=generated.tf

Rules from the generating-configuration page:

  • You still write the import block (to + id / identity).
  • If that address has no resource block yet, Terraform writes a best-guess HCL template to a new file path. An existing file path is an error.
  • The file is a starting point. HashiCorp tells you to remove arguments, adjust values, and move blocks into modules before commit.
  • Complex schemas can emit conflicting arguments (two mutually exclusive fields both populated). Terraform may still write the file; you delete one argument and plan again.
  • The plan output still warns that config generation is experimental and that formatting may change.

That is the whole 1.12 generate story. Later product lines added query/list bulk search (terraform query, .tfquery.hcl) as a separate workflow; 004 / Terraform 1.12 does not require you to treat bulk search as the default import path. Do not answer 7a with terraform query.

After import, configuration must match

Import copies current remote attributes into state. The next plan compares your resource block to that state and to a refresh. If you imported a bucket named app-logs-004-example but the block sets bucket = "something-else", Terraform plans a change (often a force-new). If you omit a non-default argument, Terraform plans to set the default. Empty-ish resource blocks are a common way to accidentally "fix" a live object.

Workflow that 004 wants:

  1. Confirm the provider documents import and the ID (or identity) format.
  2. Write the resource block with the arguments you intend to manage.
  3. Add the import block (or run the CLI command).
  4. terraform plan until the import is clean — or until remaining changes are deliberate.
  5. terraform apply (block path) or accept that the CLI already wrote state.
  6. Commit the reviewed HCL. Do not commit a raw experimental dump you never read.

Scenario: the console-created bucket

Ava created app-logs-004-example in the AWS console last year. The team now wants it in the logging workspace. She adds resource "aws_s3_bucket" "logs" plus an import block with id = "app-logs-004-example". terraform plan shows 1 to import and two in-place updates because versioning and tags in the console do not match omitted defaults. She copies those arguments into the resource block. The next plan is import-only. apply binds the bucket. She leaves the import block in imports.tf as a record. She does not run terraform import a second time to a different address.

004 traps for objective 7a

  • Import does not create or destroy the remote object; it binds it.
  • CLI import does not generate HCL. The import block plus experimental -generate-config-out is the generate path, and the output is a template.
  • After import, mismatched configuration will change the object on the next plan/apply.
  • identity is a 1.12 alternative to id, not a second required argument.
  • One remote object, one address.
  • terraform query / bulk list files are later than the 1.12 exam surface; do not overclaim them.
Test Your Knowledge

On Terraform 1.12, what is the modern, reviewable way to bring an existing object under management?

A
B
C
D
Test Your Knowledge

A successful import finishes, then the next terraform plan proposes in-place updates on that same object. What is the usual cause?

A
B
C
D
Test Your Knowledge

What does the legacy command terraform import aws_instance.web i-abcd1234 do, and what does it not do?

A
B
C
D