Terraform Import Blocks: Adopt Existing Infrastructure Through Reviewed Plans
Bring unmanaged cloud resources under Terraform with import blocks, stable addresses, provider-specific IDs, safe plans, and a repeatable adoption workflow.
Importing an existing cloud resource into Terraform is a state-adoption operation, not a creation operation. Terraform must learn that a real remote object belongs to a resource address in configuration. A successful import does not by itself produce a complete, maintainable resource definition, prove that the configuration matches every remote setting, or establish that future plans are harmless.
Configuration-driven import blocks make the adoption request reviewable in version control and part of the ordinary plan/apply workflow. Used carefully, they let a team inventory a resource, choose its Terraform address, preview the state change, and apply it through the same controls used for other infrastructure changes. The important safety property is not the syntax of the block: it is that each remote object has one deliberate owner, the address is stable, and the reviewed plan contains only intended actions.
Separate the three things an import needs
An adoption change connects three distinct identities:
- The remote object, identified using an ID or identity format supported by its provider.
- The Terraform resource address that should own the object, such as
aws_s3_bucket.logsormodule.network.aws_subnet.private["zone-a"]. - The resource configuration that describes how Terraform should manage the object after import.
An import block supplies the first two. A corresponding resource block supplies the third. Keep all three aligned. If the remote identifier is wrong, the provider may reject the import or resolve a different object. If the address is wrong or unstable, later refactors can detach ownership from the intended resource. If the resource configuration is incomplete or inconsistent, the first normal plan may propose updates, replacements, or deletions after the import.
For a single resource, the shape is straightforward:
import {
to = aws_s3_bucket.logs
id = "company-production-logs"
}
resource "aws_s3_bucket" "logs" {
bucket = "company-production-logs"
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
The example uses an S3 bucket name as the provider-specific import ID. Do not assume that another resource type uses a name, ARN, URL, or the same identity format. Check the provider documentation for the exact import identifier and any composite ID syntax. Current Terraform import blocks also support an identity object for provider resource types that expose identity-based imports; id and identity are alternatives, not fields to combine in one block.
The address in to must resolve to a resource instance in the configuration. That includes module paths and instance keys for resources using count or for_each. When importing to a module, write the full destination address, for example module.edge.aws_security_group.this. For a keyed resource, use the exact key, such as aws_s3_bucket.managed["logs"]. Choose keys that represent durable logical identity, not values likely to be renamed during routine refactors.
Prefer import blocks for reviewed, repeatable adoption
The imperative terraform import ADDRESS ID CLI command associates an existing object with one address in the selected state. It remains useful for controlled recovery and some specialized workflows, but it imports one resource at a time and does not author the resource configuration. Configuration-driven import blocks put the destination and ID in HCL, where reviewers can see the intended mapping before state changes.
A safe adoption change is usually a small pull request or change set with:
- The exact resource address and provider-specific identifier.
- A
resourceblock for every object that will be adopted. - Provider and module versions resolved as they will be in normal operation.
- A saved or captured plan that reviewers can inspect for all imported, changed, replaced, and destroyed resources.
- An operator and execution window agreed with the team that owns the target state.
- A follow-up plan after import that demonstrates convergence without unexpected infrastructure changes.
Keep adoption separate from unrelated refactors and infrastructure modifications. Combining a first-time import with renaming, module reshaping, provider upgrades, and policy changes makes it harder to identify which action caused an unexpected plan. If the adoption needs an address change, use a deliberate refactor workflow with the appropriate moved configuration or state operation, and verify the plan after that mapping is explicit.
The block may remain in the configuration as documentation of how the resource entered the state. Terraform recognizes that the destination is already managed and will not repeatedly import it. Teams may also remove completed import blocks after the state change is applied, provided the resource declaration and state binding remain intact. Decide which convention the repository uses and keep it consistent; an import block is not a substitute for a resource block.
Make the first plan an inventory, not a rubber stamp
Before writing HCL, inventory the remote resource using the cloud provider’s read-only APIs or console. Record its provider account or subscription, region, immutable ID, ownership tags, dependencies, lifecycle constraints, and configuration that Terraform is expected to manage. Confirm that the CLI workspace and backend correspond to the intended environment. A resource with a familiar display name can exist in more than one account or region.
Then initialize the working directory and create a plan:
terraform init
terraform validate
terraform plan -out=adoption.tfplan
terraform show adoption.tfplan
Treat the plan as a proposal requiring review. Look for the import action and verify the address and remote ID. Also inspect every other action. Terraform can refresh the discovered object and compare it with the resource configuration; the same plan may therefore include updates as well as imports. A green command exit or the phrase “will be imported” does not mean there are no consequential changes elsewhere in the plan.
For an initial adoption, a useful approval criterion is that every imported object is expected and that there are no unreviewed creates, updates, replacements, or destroys. If the provider reports differences, determine whether the configuration should reflect the current remote value or whether the change is explicitly intended. Add provider-supported arguments, defaults, lifecycle rules, and dependencies as appropriate, then plan again. Do not suppress a destructive action merely to make the first apply pass; understand whether the proposed replacement is correct before proceeding.
When the plan has been reviewed, apply the exact saved plan through the repository’s normal approval process:
terraform apply adoption.tfplan
If the plan becomes stale because the configuration or state changes after review, create and review a fresh plan. Do not bypass that control with an unreviewed apply. After the import succeeds, run a new ordinary plan. The intended steady state is a plan with no unexpected changes, not just an import message from the previous run.
Import keyed collections without losing identity
For a small, known collection, an import block can use for_each to make a group of imports explicit. The values used to compute the import set and destination addresses must be available during planning. Keep the map stable and make its keys match the Terraform instance keys:
locals {
existing_buckets = {
logs = "company-production-logs"
archive = "company-production-archive"
}
}
resource "aws_s3_bucket" "managed" {
for_each = local.existing_buckets
bucket = each.value
}
import {
for_each = local.existing_buckets
to = aws_s3_bucket.managed[each.key]
id = each.value
}
The logs key maps to aws_s3_bucket.managed["logs"]; archive maps to aws_s3_bucket.managed["archive"]. This makes the association inspectable and avoids relying on list positions that can shift when an item is added or reordered. In a real module, the data structure may need separate fields for the stable Terraform key and the provider’s import ID. Do not use an object name as a Terraform key if renaming that object should not change its logical address.
For larger estates, first produce an authoritative inventory and compare it with existing state. Terraform has separate bulk discovery and import workflows for large sets of unmanaged resources. They are not a reason to feed an unreviewed discovery result directly into production state. Review the generated addresses, import IDs, and resource configuration; split the work into bounded batches that are small enough to inspect and recover.
Avoid trying to import one remote object to multiple addresses. Terraform expects each remote object it manages to be bound to one resource address. Duplicate bindings can lead to confusing plans and unsafe operations. Before applying, search the current state and relevant workspaces for an existing binding, and verify that the intended address is not already occupied by a different object.
Know what import does not do
Import does not create the remote resource. It also does not make the resource configuration automatically complete. The provider reads the object and records its binding and attributes in state, but you still own the HCL that expresses desired configuration. An empty or minimal resource block may be sufficient to begin an import for some providers, but it is not a durable production definition by itself.
Some provider resources have complex import behavior. Importing one parent may expose related or secondary objects that the provider also records. If Terraform reports additional imported instances, define and review configuration for every object that should remain managed. Leaving an imported object without a corresponding resource declaration can cause a later plan to propose destroying it. Read the resource-specific import documentation and the full plan output rather than assuming the top-level resource is the whole operation.
Configuration generation can help bootstrap HCL for supported resources, but generated code is only a starting point. Verify required arguments, optional defaults, computed attributes, mutually exclusive fields, references, tags, lifecycle behavior, and organization conventions. Generated values may describe the present remote object without expressing the durable intent the team wants to preserve. Keep generated files out of the production change until a human has reviewed and normalized them.
Import also does not transfer every surrounding responsibility automatically. It does not establish a module boundary, determine which team owns the service, document dependencies, reconcile monitoring and backup policies, or update a deployment pipeline. Make those ownership and operational relationships explicit in the same repository or the relevant service documentation.
Protect the state transaction
An import changes Terraform state, so coordinate it like another state-writing operation. Use the correct workspace or backend, preserve the plan and apply records, and avoid a simultaneous apply to the same state. Remote backends that support state locking use locks to prevent concurrent writers during operations that could write state. Not every backend supports locking, so confirm the backend’s behavior and follow the team’s change window.
Never edit terraform.tfstate directly to simulate an import. Use Terraform’s supported configuration and state commands. Before a recovery operation, stop competing runs, identify the exact workspace and resource address, capture a protected backup or backend version according to the team’s procedure, and understand whether the intended operation changes only Terraform’s record or also the real object.
If an import is applied to the wrong address, do not immediately destroy or recreate the remote infrastructure. First stop further applies and compare the remote object, state binding, configuration, and plan. A state removal command can disassociate an object without deleting it, but it is a state mutation that must be performed deliberately, with the target workspace verified and a correct replacement mapping prepared. A removed block may be more reviewable for supported workflows. Re-plan before and after any correction, and ensure there is one authoritative address at the end.
A production adoption checklist
- Confirm the account, region, provider configuration, backend, workspace, and current state before choosing an ID.
- Read the resource-specific import documentation; do not guess the ID format.
- Define the intended resource address and complete destination resource configuration in code review.
- Verify that no other address or workspace already manages the same remote object.
- Use stable
for_eachkeys for batches and ensure all plan-time values are known. - Review the full plan, including secondary imports and every non-import action.
- Apply only the reviewed plan through the team’s normal approvals and state-locking controls.
- Run a fresh plan after applying and resolve drift or unintended updates before declaring adoption complete.
- Record the owner, import convention, follow-up work, and any provider-specific limitations.
An import is complete when the remote object has one deliberate Terraform owner, the configuration describes the team’s intended management contract, and a fresh plan is predictable. Treating adoption as a reviewed state transition, rather than a one-line CLI command, keeps existing infrastructure from becoming an accidental source of drift or replacement.
Related:
- Terraform State in Production: Backends, Locking, Drift, Import, and Recovery
- Terraform for_each: Stable Resource Identity, Safe Refactors, and Plan-Time Keys
Sources: