s because it repositions state import from a risky operational task to a standard engineering workflow. Declarative blocks enable pull request reviews, automated plan validation, and team coordination. Configuration generation reduces the manual mapping burden from hours to minutes, provided teams treat the output as a draft rather than a final artifact. The scalability improvement is not just about speed; it is about predictability. When imports participate in the standard plan/apply cycle, teams gain visibility into attribute mismatches, provider version conflicts, and state boundary issues before any mutation occurs.
Core Solution
Reconciling legacy resources requires a four-phase workflow: inventory resolution, declarative declaration, configuration scaffolding, and validation. Each phase addresses a specific failure mode in traditional import practices.
Phase 1: Inventory & ID Resolution
Every provider defines its own import ID convention. An EC2 instance uses an instance ID, an RDS cluster uses an ARN, and an IAM policy attachment requires a composite string. Resolving these IDs manually is the primary bottleneck. Instead of querying consoles or parsing CLI output, teams should extract IDs from existing inventory systems, cloud resource tags, or automated discovery scripts.
# inventory.tf
locals {
legacy_resources = {
analytics_db = {
type = "aws_rds_cluster"
id = "arn:aws:rds:us-east-1:123456789012:cluster:prod-analytics"
}
api_gateway = {
type = "aws_apigatewayv2_api"
id = "a1b2c3d4e5"
}
}
}
Phase 2: Declarative Import Declaration
Terraform 1.5+ allows imports to be declared as top-level blocks. These blocks participate in the plan phase, showing exactly what will be registered before any state mutation occurs.
import {
id = local.legacy_resources.analytics_db.id
to = aws_rds_cluster.analytics
}
import {
id = local.legacy_resources.api_gateway.id
to = aws_apigatewayv2_api.main
}
For environments managing dozens of similar resources, OpenTofu 1.7+ supports iteration directly within import blocks. This eliminates repetitive declarations and reduces typo surface area.
locals {
monitoring_rules = {
cpu_alert = "sg-rule-0a1b2c3d4e5f6a7b8"
memory_alert = "sg-rule-9z8y7x6w5v4u3t2s1"
disk_alert = "sg-rule-1a2b3c4d5e6f7g8h9"
}
}
import {
for_each = local.monitoring_rules
id = each.value
to = aws_security_group_rule.monitoring[each.key]
}
resource "aws_security_group_rule" "monitoring" {
for_each = local.monitoring_rules
# ... rule configuration
}
Phase 3: Configuration Generation & Refinement
Running terraform plan -generate-config-out=imported.tf instructs the provider to emit a best-guess HCL configuration for every import block lacking a matching resource definition. The generated file contains attribute mappings, type hints, and structural scaffolding.
Architecture Decision: Why use generation instead of manual authoring? Provider schemas contain hundreds of attributes, many with implicit defaults or computed values. Manual mapping introduces version drift and attribute omission. Generation ensures schema compliance. The tradeoff is verbosity: generated configurations often include redundant or mutually exclusive arguments. Teams must treat the output as a draft, removing computed fields, consolidating defaults, and aligning naming conventions with existing module structures.
Phase 4: Validation & Application
The import blocks remain active until the resource is fully reconciled. Running terraform plan should show a zero-diff state after configuration refinement. If diffs appear, they indicate attribute mismatches, provider version incompatibilities, or missing dependencies. Only after plan validation should terraform apply execute the state registration.
Why this architecture works: Declarative imports decouple state mutation from execution timing. The plan phase acts as a safety gate, catching configuration gaps before state changes. CI pipelines can validate import plans automatically, preventing unauthorized state modifications. Once applied, import blocks become obsolete and should be removed to prevent idempotency conflicts.
Pitfall Guide
1. Importing Without Matching Configuration
Explanation: Running an import block without a corresponding resource definition leaves Terraform with state data but no desired configuration. Subsequent applies will attempt to destroy or recreate the resource to match the empty config.
Fix: Always pair import blocks with resource stubs. Use -generate-config-out to scaffold the configuration, then refine it before applying.
2. Leaving Import Blocks in Version Control
Explanation: Import blocks are not idempotent. Once a resource is registered, the block becomes noise and may trigger errors on subsequent plans if the resource already exists in state.
Fix: Remove import blocks immediately after successful application. Use CI pipelines to enforce cleanup, or maintain imports in a separate imports/ directory that is deleted post-migration.
3. Ignoring Provider Version Compatibility
Explanation: Import operations rely on provider schemas to map live attributes to HCL. If the provider version in your configuration differs from the version that originally created the resource, schema mismatches will cause plan failures or attribute loss.
Fix: Pin provider versions during import operations. Verify the provider version matches the resource creation era, or run a schema migration plan before importing.
4. Overlooking Computed vs. Required Attributes
Explanation: Generated configurations often include computed attributes like arn, id, or created_at. Terraform rejects these in configuration because they are managed by the provider, not the user.
Fix: Strip all computed fields from generated HCL. Retain only user-configurable attributes. Use terraform plan to identify which attributes trigger "computed value in config" errors.
5. Skipping Post-Import Drift Baselines
Explanation: Importing a resource registers its current state but does not prevent future manual modifications. Without ongoing drift detection, resources will gradually diverge from configuration again.
Fix: Implement scheduled terraform plan jobs in CI that fail on non-zero diffs. Tag imported resources with metadata indicating their migration date for audit trails.
6. State File Fragmentation Mismanagement
Explanation: Large organizations split state across multiple files or workspaces. Importing resources into the wrong state file creates boundary conflicts and makes refactoring difficult.
Fix: Map resources to state boundaries before importing. Use terraform state mv to relocate resources if they land in the wrong file. Document state partitioning rules in team runbooks.
7. Assuming Generated Config Is Production-Ready
Explanation: Auto-generated HCL prioritizes schema completeness over readability. It often includes explicit defaults, redundant arguments, and naming conventions that clash with team standards.
Fix: Treat generated output as a draft. Refactor into existing modules, remove explicit defaults, align naming with conventions, and validate against peer review standards.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Single resource recovery | Declarative import block + manual config | Low overhead, full reviewability | Minimal engineering time |
| Bulk import of identical resource type | OpenTofu for_each import blocks | Eliminates boilerplate, reduces typo risk | Moderate setup, high scalability |
| Cold-start migration (zero IaC) | Automated generator (tfimport, Terraformer, aztfexport) | Rapid scaffolding, inventory-driven | High cleanup effort, fast initial coverage |
| Multi-account/state partitioned environment | Inventory mapping + state boundary validation | Prevents state fragmentation, maintains isolation | Requires architectural planning |
| Strict compliance/audit requirements | CI-gated import blocks + plan validation | Enforces review, prevents unauthorized mutation | Adds pipeline complexity, ensures compliance |
Configuration Template
# imports/migration.tf
locals {
target_resources = {
primary_db = {
resource_type = "aws_rds_cluster"
import_id = "arn:aws:rds:us-east-1:123456789012:cluster:prod-primary"
}
cache_cluster = {
resource_type = "aws_elasticache_cluster"
import_id = "prod-cache-001"
}
}
}
import {
for_each = local.target_resources
id = each.value.import_id
to = resource(each.value.resource_type, each.key)
}
# Run: terraform plan -generate-config-out=generated.tf
# Refine generated.tf, remove computed attrs, align with modules
# Apply, then delete this file
Quick Start Guide
- Create a migration branch and add a dedicated
imports/ directory to isolate import blocks from production configuration.
- Define your inventory using a
locals map containing resource types and provider-specific import IDs.
- Write declarative import blocks referencing the inventory. Use
for_each if available in your IaC platform.
- Generate configuration by running
terraform plan -generate-config-out=generated.tf. Review the output, strip computed fields, and align with your module structure.
- Validate and apply by running
terraform plan to confirm zero diff, then terraform apply to register state. Delete the import blocks immediately after success.