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
12.1 Import Existing Infrastructure
Quick Answer: Write a
resourceblock for the already-existing object. Add animportblock whosetoaddress matches that resource and whoseid(or, on Terraform 1.12,identity) names the live object. Runterraform plan, fix any proposed updates until the plan is import-only, thenterraform apply. The legacyterraform import ADDRESS IDcommand 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:
| Argument | Required? | Role on Terraform 1.12 |
|---|---|---|
to | Yes | Destination address (TYPE.LABEL, plus an index if count/for_each, or module.NAME....) |
id | One of id or identity | Provider-specific import ID; must be known at plan time |
identity | One of id or identity | New in 1.12.0. Object of provider-defined keys that uniquely identify the resource. Mutually exclusive with id |
for_each | No | Import many similar objects from a map or set |
provider | No | Non-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/apply | terraform import ADDRESS ID | |
|---|---|---|
| When it landed | Terraform 1.5 language | Long-standing CLI |
| Writes state | On apply after a reviewed plan | Immediately on success |
| Generates HCL? | Only if you add experimental -generate-config-out | Never |
| Many resources | Multiple blocks or for_each | One address per invocation |
| CI / review | Same workflow as other changes | One-shot, easy to skip review |
| HCP Terraform | Import action can run in the remote plan/apply | Command 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
importblock (to+id/identity). - If that address has no
resourceblock 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:
- Confirm the provider documents import and the ID (or identity) format.
- Write the
resourceblock with the arguments you intend to manage. - Add the
importblock (or run the CLI command). terraform planuntil the import is clean — or until remaining changes are deliberate.terraform apply(block path) or accept that the CLI already wrote state.- 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
importblock plus experimental-generate-config-outis the generate path, and the output is a template. - After import, mismatched configuration will change the object on the next plan/apply.
identityis a 1.12 alternative toid, 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.
On Terraform 1.12, what is the modern, reviewable way to bring an existing object under management?
A successful import finishes, then the next terraform plan proposes in-place updates on that same object. What is the usual cause?
What does the legacy command terraform import aws_instance.web i-abcd1234 do, and what does it not do?