# Import Existing Infrastructure into Terraform Without Hand-Writing HCL

> How Terraform import blocks and the -generate-config-out flag turn a painful manual chore into a reviewable, config-driven workflow — plus the limits that still bite.

URL: https://www.matthewswong.com/en/blog/terraform-import-blocks-existing-infra/

- Author: Matthews Wong (https://www.matthewswong.com/en/about/)
- Published: 2026-07-11
- Updated: 2026-07-11
- Category: DevOps

> **Key takeaway:** Terraform 1.5 added import blocks: declarative import you write in your config, see in terraform plan, and review like any code change. Pair them with the -generate-config-out flag and Terraform writes best-guess HCL for each imported resource, so you refine drafts instead of hand-authoring every argument from scratch.

Almost every infrastructure job I take on starts with the same problem: something already exists. A load balancer clicked together in a console two years ago, a database created during an incident, a DNS zone nobody remembers touching. Terraform is happiest with greenfield, but real life is brownfield. The question is never whether to bring existing infrastructure under Terraform — it is how much pain that migration costs.

For years the answer was: a lot. The old terraform import CLI command only wrote a resource into state — it never generated the configuration to match. You imported the resource, then hand-wrote the HCL, then ran plan and prayed the diff came back empty. On anything larger than a handful of resources that loop was exhausting and error-prone. Since Terraform 1.5, import blocks plus the -generate-config-out flag change the shape of that work entirely.

## The old way and why it hurt

The classic CLI import is imperative: you run a command, it mutates state immediately, and it leaves no trace in your configuration or your pull request. Two problems fall out of that. First, there is nothing to review — a teammate cannot see in a diff what you imported. Second, you still have to write every argument of the resource yourself, matching whatever the cloud provider actually has, or your next plan shows a destructive change.

## Import blocks: config-driven import

An import block is a normal Terraform block you commit to a .tf file. It names the resource address you want the imported object to live at (the to argument) and the provider-specific ID of the existing object (the id argument). Because it lives in your configuration, it shows up in terraform plan, it goes through code review, and nothing touches state until terraform apply. That is the whole shift: import becomes declarative and reviewable instead of a one-off console command.

> Keep import blocks in a dedicated file like imports.tf. They are single-use scaffolding — once the apply lands and the resource is in state, the block does nothing on future plans and you can delete it in a follow-up PR. Grouping them makes that cleanup a one-file change.

## -generate-config-out: stop hand-writing HCL

The import block on its own still assumes a matching resource block exists. The magic is the -generate-config-out flag, introduced alongside import blocks in Terraform 1.5 and still labelled experimental. Point terraform plan at a new file path and Terraform reads the live object for every import block that has no matching resource, then writes HCL with its best guess for each argument. You get a scaffold, not a finished module — but a scaffold beats a blank file.

1. Write import blocks for the resources you want, with the correct to address and id.
2. Run terraform plan -generate-config-out=generated.tf against a fresh file path.
3. Read the generated HCL, delete read-only and default arguments, and refactor names.
4. Run terraform plan again until the diff is empty — no create, no destroy.
5. terraform apply to commit the objects to state, then delete the import blocks.

## The generated HCL is a draft, not a deliverable

Do not commit the output of -generate-config-out untouched. Terraform errs toward completeness, so it emits read-only attributes, computed values, and defaults that clutter the file or actively break the plan. The docs call out a concrete example: it can generate both ipv6_address_count and ipv6_addresses on an instance even though those two arguments conflict, producing HCL that will not validate. Treat the file as a first draft you clean up.

- Delete arguments the provider marks read-only or computed (IDs, ARNs, timestamps).
- Remove defaults that add noise, so the config states intent, not every knob.
- Replace hard-coded IDs with references to other resources or variables where possible.
- Rename the resource label to your convention — the generated name is just the to address.

> -generate-config-out has real limits. It cannot generate config for import blocks that use for_each, or for resources using for_each or count, and it does not work with the remote backend or HCP Terraform because it must write to the local filesystem. Terraform 1.7 added for_each support to the import block itself, but config generation for those instances still is not covered — generate against a plain top-level address first, then refactor into your collection by hand.

## A workflow that scales past a handful of resources

On a recent migration I had a couple of dozen objects to bring in across a single provider. The pattern that worked: enumerate the real IDs with the provider CLI, template one import block per ID into imports.tf, then run a single generate pass. You review the generated file in one sitting, clean it, and land it as one reviewable PR — far better than a shell loop of terraform import commands nobody can audit after the fact.

| Aspect | terraform import (CLI) | import block + generate-config-out |
| --- | --- | --- |
| Style | Imperative, runs immediately | Declarative, applied on apply |
| Reviewable in a PR | No — leaves no config trace | Yes — shows up in plan and diff |
| Generates config | Never — state only | Yes — best-guess HCL draft |
| Bulk import | One command per resource | Many blocks, one plan pass |
| for_each / count | Manual, per instance | Block supports for_each (1.7+); gen does not |
| Remote backend / HCP | Works | generate-config-out needs local FS |

## Gotchas I keep hitting

- The output path must be new. Terraform errors rather than overwriting an existing file — delete generated.tf between runs.
- Getting the id right is provider-specific. An S3 bucket uses its name, an EC2 instance its instance ID, a route53 record a composite string — check the resource's import docs.
- Config generation is still experimental; the exact HCL formatting can change between minor versions, so re-review after upgrades.
- Import to a module or a for_each instance? Generate against a top-level address first, then move the cleaned HCL into the module by hand.

> The real win is not saved keystrokes — it is auditability. Import becomes a pull request your team can review, approve, and roll back, with the generated config as a starting point. That single change moved importing brownfield infrastructure from a scary console chore to a normal, boring part of my IaC workflow.

## Sources

- Terraform Docs — Import: Generating Configuration
- Terraform Docs — Import resources overview
- HashiCorp Blog — Terraform 1.5 brings config-driven import and checks

## Source links

- https://developer.hashicorp.com/terraform/language/import/generating-configuration
- https://developer.hashicorp.com/terraform/language/import
- https://www.hashicorp.com/en/blog/terraform-1-5-brings-config-driven-import-and-checks

## FAQ

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

The terraform import CLI command is imperative: it writes a resource into state immediately and generates no configuration, leaving no trace in your code. An import block is declarative — you commit it to a .tf file, it appears in terraform plan, goes through code review, and nothing changes in state until terraform apply. Import blocks were added in Terraform 1.5.

### What does the -generate-config-out flag do in Terraform?

When you run terraform plan -generate-config-out=file.tf, Terraform reads each existing object referenced by an import block that has no matching resource, then writes HCL with its best guess for every argument to that file. It gives you a scaffold of the resource configuration instead of a blank file, so you refine a draft rather than hand-writing everything. It was introduced in Terraform 1.5 and is still experimental.

### Can I use -generate-config-out with for_each or count?

No. Config generation cannot be used with import blocks that use for_each, or with resources that use for_each or count, and it does not work for resources inside modules that do not already exist. Terraform 1.7 added for_each support to the import block itself, but config generation for those instances is still not covered. Generate against a plain top-level resource address first, then refactor the cleaned HCL into your collection or module by hand.

### Why does terraform plan error when I use -generate-config-out?

The most common cause is pointing the flag at a file that already exists — Terraform refuses to overwrite it and errors, so delete the generated file between runs. It also fails with the remote backend or HCP Terraform because the flag must write to the local filesystem. Finally, generated config can include conflicting arguments (for example ipv6_address_count and ipv6_addresses together) that fail validation until you edit them.

### Do I need to keep import blocks in my Terraform configuration forever?

No. Import blocks are single-use scaffolding. Once terraform apply commits the object to state, the block does nothing on future plans, so you can safely delete it in a follow-up pull request. Keeping them in a dedicated file like imports.tf makes that cleanup a one-file change.

---

- HTML version: https://www.matthewswong.com/en/blog/terraform-import-blocks-existing-infra/
- Bahasa Indonesia: https://www.matthewswong.com/id/blog/terraform-import-blocks-existing-infra/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
