Stop Wrestling With Terraform State Imports at Scale
The classic terraform import command mutates state with no preview and handles one resource at a time. Here is how config-driven import blocks, generated configuration, and helper tooling change that calculus.
Somewhere in every organization that uses Terraform, there is a sprawling set of cloud resources that pre-date the IaC adoption push. They were provisioned by hand, by a previous engineer, by a now-deprecated internal tool, or by someone who ran aws ec2 run-instances at 2 a.m. during an incident and never looked back. The resources work fine. The problem is that Terraform does not know they exist, which means every subsequent plan has drift potential, and any refactor risks destroying something that cannot be recreated cleanly.
Bringing those resources under Terraform control is called a state import. And for years, the experience of doing it at scale has been genuinely painful.
Why the legacy terraform import command breaks at scale
The original terraform import command — available since the early days of Terraform and still present in Terraform 1.x and OpenTofu — does one thing: it writes a resource’s current state into your .tfstate file, mapping it to a resource address you specify. The syntax is simple enough:
terraform import aws_instance.web i-0a1b2c3d4e5fThe problem shows up when you have fifty resources to import, or five hundred.
First, the command is sequential by design. You run it once per resource. There is no native way to hand it a list and walk away. If your environment has a hundred EC2 instances, you are writing a hundred commands — each with a provider-specific ID format you have to look up per resource type.
Second, the command modifies state immediately with no plan step. There is no diff, no review, no pull request gate. The moment you run it, your state file changes. If another team member runs terraform apply on that shared state before you have added matching HCL configuration, Terraform will try to destroy or modify what you just imported because the resource exists in state but not in configuration.
Third, you still have to write the HCL yourself. The import command does not generate configuration. After importing a resource, your terraform plan will show a diff between state (what the resource actually looks like) and configuration (what you wrote, which is probably incomplete). Closing that diff manually — finding every attribute, getting the types right — is the part that takes most of the time.
Config-driven import blocks: what changed in Terraform 1.5
Terraform 1.5 (released mid-2023) introduced a new top-level import block that addresses the preview and sequencing problems. Instead of running a CLI command that mutates state, you declare your imports in HCL:
import { id = "i-0a1b2c3d4e5f" to = aws_instance.web}This block participates in the normal plan/apply cycle. When you run terraform plan, Terraform reads the live resource state, shows you what will be imported, and gives you a chance to review the diff before anything is committed. The import only happens on terraform apply. That means you can open a pull request with your import blocks, get review, and let CI validate the plan — the same workflow you use for any other infrastructure change.
You can also chain the import block with Terraform’s config generation feature. Running:
terraform plan -generate-config-out=imported.tftells Terraform to emit a .tf file containing a best-guess resource block for every resource referenced by an import block that does not already have a matching configuration block. This dramatically reduces the manual work. Instead of handwriting the HCL for an RDS instance with 40 arguments, you let Terraform generate a starting point and then edit it.
A few caveats apply. The generated configuration is explicitly described as experimental in the official documentation — the formatting may change between minor releases, and some resource types generate mutually exclusive arguments (for example, computed and user-provided values that are semantically redundant) that you have to remove manually before the plan will succeed. The generated output is a starting point, not a finished product. Plan carefully, check that your next terraform plan after applying the import shows no diff, and remove the import blocks once the migration is complete (they are not idempotent in the same way resource blocks are — they apply once and then become noise).
OpenTofu goes further with for_each on import blocks
OpenTofu, the open-source Terraform fork maintained by the Linux Foundation, has been adding capabilities on top of the 1.5 baseline. OpenTofu 1.7 added support for for_each on import blocks, which is a meaningful ergonomic improvement when you are importing multiple resources of the same type:
locals { buckets = { "logs" = "my-org-logs-bucket" "backups" = "my-org-backups-bucket" "assets" = "my-org-static-assets" }}
import { for_each = local.buckets id = each.value to = aws_s3_bucket.managed[each.key]}
resource "aws_s3_bucket" "managed" { for_each = local.buckets bucket = each.value}In standard Terraform 1.5+, you would write one import block per bucket. With OpenTofu 1.7’s loopable import, you write one block and iterate. For environments importing dozens of resources of the same type — load balancer listeners, security group rules, IAM roles — this significantly reduces the boilerplate and the surface area for typos.
If you are on the Terraform side and need similar behavior, the practical workaround is to generate the import blocks programmatically — a shell loop, a Python script, or a tool that reads from your existing infrastructure inventory.
Helper tooling: what tfimport and similar tools cover
The remaining hard part is ID resolution. Every Terraform provider has its own convention for what constitutes a valid import ID. An EC2 instance is its instance ID. An AWS IAM role policy attachment is role/policy_arn. An ALB listener rule is just the rule ARN. A security group rule is a computed string that looks like sgr-04966a7c7b7a94e19. You have to look up the correct format in provider documentation for each resource type, then find the actual value in the AWS console or via CLI.
This is where third-party tools add their value. tfimport (github.com/coolapso/tfimport) is a Go-based CLI that automates exactly this step. It reads your Terraform plan to discover which resources need importing, then resolves the correct provider-specific import ID for each resource — using SDK lookups against the cloud API where necessary — and either generates import blocks or runs the terraform import commands for you. It supports Terraform, Terragrunt, and OpenTofu, and handles rate limiting between imports via a configurable delay flag.
A rough workflow looks like this:
# Generate import blocks for all unmanaged resources in the plantfimport
# Or run imports directly via CLItfimport --run-import
# With Terragrunttfimport --tg
# Skip specific resourcestfimport --ignore "aws_iam_role.legacy_*"There are also cloud-native alternatives for specific providers. Azure has aztfexport, a Microsoft-maintained tool that scans existing Azure resources and generates matching Terraform HCL. Google Cloud has Terraformer (github.com/GoogleCloudPlatform/terraformer), which does reverse-Terraform for GCP, AWS, and several other providers. These tools generate both configuration and import blocks, which makes them useful for a cold-start migration where you have nothing at all in Terraform yet.
The tradeoff with generated-everything approaches is that you end up owning whatever the tool produces. Generated HCL tends to be verbose — every optional attribute explicitly set, no module abstractions, naming derived from resource IDs rather than your team’s conventions. You will spend time cleaning it up. That cleanup is less work than writing it from scratch, but it is not zero.
What none of these approaches solve automatically
Importing resources into Terraform state is not the same as integrating them into a well-structured Terraform codebase. After import, you typically still need to:
- Refactor the generated code into your existing module structure.
- Remove attributes Terraform manages as computed (things like
arn,id, certain timestamps) that should not appear in configuration. - Verify that the next
terraform planafter import shows zero diff — any diff means the imported state and your configuration disagree, and applying will change the resource. - Decide which resources belong in which state file if you are using workspaces or Terragrunt with split state.
The drift detection problem also persists after import. Bringing a resource into state does not prevent someone from changing it in the console next week. You need a plan check in CI — scheduled terraform plan runs that fail on non-zero diff — to catch that.
The tooling covered here reduces the mechanical cost of the import operation itself. It does not replace the architecture work of deciding how to organize your configuration, nor does it automatically enforce going-forward discipline. Both matter at least as much as the import tooling.
FAQ
Do I need to remove import blocks after applying? +
Does every Terraform resource type support import? +
Can I import resources into a remote state backend like Terraform Cloud or S3? +
Related tools
Beehiiv
Newsletter platform with built-in ad network and Boost referrals.
Try Beehiiv →
Webflow
Visual site builder with real CSS export and a CMS that scales.
Try Webflow →
Some links above are affiliate links. We may earn a commission if you sign up. See our disclosure for details.
Related reading
2026-05-21
AI-Powered Observability: Querying Telemetry in Plain English
Observability platforms now let you ask questions of logs, metrics, and traces in natural language. Here's how the translation layer works, what it genuinely helps with, and where it breaks.
2026-05-21
Caddy Web Server Review: Automatic HTTPS Without the Ceremony
A detailed look at Caddy's automatic TLS, Caddyfile syntax, and reverse proxy setup — and where it falls short compared to Nginx.
2026-05-21
Mac Mini as AI Agent Infrastructure: Why Apple Silicon Powers Local LLM Inference
How Apple Silicon's unified memory architecture makes the Mac Mini a practical local inference node — benchmarks, real costs, setup with Ollama and MLX, and honest tradeoffs versus cloud GPUs.
2026-05-21
NixOS & nixpkgs in 2026: Reproducible Dev Environments Without Docker
How Nix flakes and devShells replace Docker for local dev: what works, where it hurts, and whether the learning curve is worth it for your team.
2026-05-21
The Rust Sidecar Pattern: Fixing Python AI's Deployment Weakness
Python dominates ML development but struggles in production serving. The Rust sidecar pattern splits responsibilities: Python handles models, Rust owns the hot path. Here's the mechanics.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.