Import existing infrastructure into Terraform without recreation
Adopt existing provider objects through Terraform 1.15 import blocks, stable addresses, optional reviewed generated HCL, import-only saved plans, protected state, and a mandatory empty post-import plan.
Bind each existing remote object exactly once to deliberate Terraform configuration without creating, changing, replacing, or destroying infrastructure during adoption, then hand it to a named owner with reproducible evidence and rollback.
- Terraform Community Edition 1.15.x
- Terraform import blocks Current language
- AWS provider 6.x example
- Supported and pinned toolchain Terraform 1.15.5 and the exact provider version are approved, initialized from immutable sources, and used consistently through discovery, plan, apply, and verification.
terraform version && terraform providers - Protected target state The target root has encrypted, versioned, locked remote state, a known workspace and lineage, no unexplained drift, and an isolated tested recovery path.
terraform workspace show && terraform state list - Read-only provider inventory The operator can identify every object, dependency, account, region, import identity, immutable field, security control, and owner without modifying it.
- Change freeze Manual consoles, automation, other Terraform roots, and scheduled drift tasks cannot modify the selected objects during the migration window.
- One-to-one ownership search Every relevant state, workspace, root, and external controller has been searched for duplicate claims to the target provider identities.
- Recovery and reviewers A protected pre-import state backup, state version identifier, service rollback owner, provider expert, and second reviewer are available.
OneLiners never runs these steps or stores secrets. Review placeholders, versions, current state, and change-control requirements before using a command.
Full guide
What you will build
- A read-only discovery inventory that maps each existing provider object to exactly one Terraform resource type, immutable provider identity, destination address, owner, dependency, sensitive field, and rollback decision.
- A configuration-driven import migration using Terraform 1.15 `import` blocks, reviewed resource configuration, an optional experimental `-generate-config-out` working artifact, and a saved plan whose only actions are imports.
- A post-import acceptance and recovery procedure that proves zero create, update, replacement, or destroy during import, converges to an empty normal plan, preserves state versions, prevents duplicate ownership, and records how each object can be released from Terraform without deleting it.
- Every selected object is bound once to the intended root or module instance address, and repository configuration accurately describes the approved remote settings without recreating the object.
- Reviewers can trace provider identity to address, source configuration, import block, saved plan, state version, owner, and post-import verification without reading raw sensitive state.
- A failed or partial migration can be stopped safely: unapplied blocks are removed, applied bindings remain protected under remote locking, and any duplicate or wrong binding is repaired through official state commands only after a backup and new plan.
Architecture
How the parts fit together
Import is a state-adoption workflow, not resource creation and not automatic reverse engineering. The authoritative remote inventory identifies objects and dependencies. The target Terraform root already uses a protected remote backend and a pinned provider. Owned HCL declares the desired resource at a stable address. An `import` block connects the provider-specific ID or supported structured identity to that address. Terraform refreshes the object during planning and displays both the import action and any infrastructure change that configuration would request. The acceptance gate requires imports only. Generated configuration can accelerate a small migration when no resource block exists, but current official documentation still calls `-generate-config-out` experimental and warns that provider schemas can yield conflicting or invalid arguments. Therefore generated HCL remains an ignored working artifact until a human removes computed fields, chooses valid argument combinations, encodes policy, and moves it into reviewed source. After apply, a normal plan must be empty before the object is considered safely managed.
- Freeze overlapping manual and Terraform changes, select a small import batch, verify provider identities read-only, and search every state boundary for duplicate ownership.
- Back up the protected remote state, pin Terraform and provider versions, initialize the correct root, and record its lineage, workspace, addresses, and backend key.
- Choose stable destination addresses and model dependencies. Use complete module and `for_each` instance addresses where applicable; do not use list positions as identity.
- Write resource configuration manually when behavior is understood. Otherwise add only import blocks and generate a new experimental HCL working file, then validate every argument against provider documentation and actual intent.
- Create a locked saved plan and require a summary of imports with zero add, change, and destroy. Inspect machine-readable actions and every provider warning.
- Apply the exact saved plan, verify state identity and remote behavior, run a fresh normal plan, and correct configuration until it is empty without weakening controls.
- Commit configuration and migration history, monitor the object, document release-without-destroy, and remove or retain import blocks through an explicit follow-up decision.
Assumptions
- Terraform Community Edition 1.15.5 and the exact pinned provider versions are used for discovery, generation, planning, import, and acceptance. Provider import identity and schema are verified for those versions.
- The target root already has a durable remote backend with locking, encryption, versioning, recoverable historical state, and no unexplained drift. Import is never attempted into an unknown or local production state.
- The operator can read each target object and related dependencies through provider-native APIs but does not need broad rights to modify unrelated infrastructure.
- The migration begins with a small batch whose owner can explain every configured field, dependency, immutable attribute, security control, and deletion consequence.
- Manual changes and other Terraform applies affecting the selected objects are frozen for the migration window. State locking serializes Terraform writers but cannot stop console or API changes.
- Generated configuration is optional and experimental. It is not committed or applied until a human reviews and converts it into deliberate owned HCL.
- The import target uses a one-to-one mapping. No other root, workspace, address, or configuration-management system continues to manage the same object after acceptance.
- A post-import empty normal plan is mandatory. An import-only summary is necessary but not sufficient because later provider refresh and complete configuration can reveal additional differences.
Key concepts
- Root module
- The configuration directory from which Terraform runs. It owns backend and provider configuration, calls child modules, supplies environment-specific values, and is the correct boundary for a state workspace.
- Child module
- A reusable collection of resources called by a root or another child module. A child module declares provider requirements but normally receives provider configurations from its caller and does not decide where state is stored.
- State binding
- The association between a Terraform resource instance address and one remote object identity. Safe operations preserve a one-to-one mapping; duplicate imports and empty or wrong state keys violate that assumption.
- Backend
- The subsystem that stores state and, depending on type, provides locking and operation features. Backend configuration is evaluated before ordinary Terraform expressions, so it cannot use normal input variables or resource outputs.
- State lock
- A serialization mechanism that prevents concurrent writers from calculating and committing changes against the same snapshot. It protects consistency only when every writer honors it.
- Saved plan
- A binary plan artifact that captures a reviewed proposal for a particular configuration, state lineage, provider set, and variable context. It is sensitive and becomes stale when any relevant input changes.
- Refresh
- The read of provider APIs that updates Terraform's in-memory understanding of remote objects before comparison. A refresh can reveal drift even when the configuration repository did not change.
- Dependency lock file
- The `.terraform.lock.hcl` record of selected provider versions and checksums. It should be committed and reviewed; it does not lock child module versions or serialize state operations.
- Configuration-driven import
- A declarative `import` block included in normal planning and apply. Unlike the legacy one-off CLI command, it is reviewable, repeatable, supports module addresses and current `for_each`, and can remain as migration history.
- Import ID
- A provider-resource-specific string that identifies one remote object. It may be a name, ARN, composite string, or another form; operators must use the current provider documentation rather than infer it.
- Resource identity
- A provider-supported map of identity attributes available for some resources and mutually exclusive with `id` in an import block. Its keys and support depend on the provider resource.
- Generated configuration
- Best-effort experimental HCL written by `terraform plan -generate-config-out` for import targets without resource blocks. It is a starting template and can contain computed, conflicting, or undesirable values.
- Adopt reality
- The deliberate choice to update configuration so the approved current remote setting becomes desired state. Merely refreshing state does not resolve a configuration mismatch.
- Restore intent
- The deliberate choice to keep configuration as authoritative and later apply a reviewed plan that changes the remote object back. This action is separate from the import-only migration.
Before you copy
Values used in this guide
{{resourceId}}Exact provider-specific immutable identity for the remote object, verified from authoritative inventory and current provider documentation.
Example: acme-prod-audit-logs{{resourceAddress}}Complete destination instance address including module path and `for_each` key where applicable.
Example: module.storage["prod"].aws_s3_bucket.this["audit"]{{awsRegion}}Reviewed region used by the root provider alias that can read the target object.
Example: eu-west-1{{importBatch}}Small reviewed batch identifier tying inventory, import blocks, plans, approvals, state versions, and verification evidence together.
Example: storage-2026-07-batch-01{{generatedPath}}New ignored working-file path for experimental configuration generation; it must not overwrite owned HCL.
Example: .migration/generated-storage-01.tf{{stateBackupPath}}secretProtected local or vault path for the pre-import state pull, with access and deletion controls equal to production state.
Example: /secure/import-storage-2026-07/pre-import.tfstateSecurity and production boundaries
- Terraform state and saved plan files are sensitive operational records. They can contain secrets, identifiers, network topology, policy documents, provider-returned attributes, and values hidden from normal CLI display. Encrypt them, restrict read as well as write access, keep them out of source control, and avoid attaching them to ordinary tickets.
- Use short-lived workload identity or identity federation for provider and backend access. Do not put access keys, session tokens, KMS material, or backend credentials in `.tf` files, `-backend-config=KEY=value` shell history, generated plans, screenshots, or example output.
- A `sensitive = true` value is redacted in selected interfaces but can still exist in state and plan files. Prefer ephemeral values and provider write-only arguments where Terraform 1.15 and the provider support them, then verify the provider schema instead of assuming a password was omitted.
- Keep state administration separate from ordinary infrastructure administration. The ability to overwrite state, restore older versions, delete lock files, or force-unlock can redirect a future apply even when the principal cannot directly modify every managed object.
- Never publish `TF_LOG=TRACE` output without security review. Provider requests, responses, environment details, and values can appear in debug logs; enable logging only for a bounded diagnostic and store the result as a secret-bearing incident artifact.
- Generated HCL can include access policies, network addresses, identifiers, encrypted values, and provider-observed metadata. Store it as a sensitive migration artifact even before it enters state or source control.
- Import does not make an insecure remote setting safe. Preserve the object during adoption, document every accepted risk, and schedule remediation as a separate reviewed plan with a rollback and service owner.
- Do not use `terraform state rm` as a casual undo. It only removes Terraform's binding and leaves the real object unmanaged; a later configuration plan may create a duplicate with cost or security consequences.
- A lifecycle `ignore_changes` rule is not a generic import stabilizer. Use it only when another named authority owns specific attributes, document that ownership, and test that it cannot conceal security drift.
Stop before continuing if
- Stop if the object identity, provider account, region, type, owner, dependencies, or current security settings cannot be proven read-only.
- Stop if any existing state, root, workspace, module, or other controller already claims the same remote object.
- Stop if the pre-import state backup, state version, lock, and exact target workspace cannot be verified.
- Stop if the import plan includes create, update, replace, or destroy in addition to the reviewed import actions.
- Stop if generated configuration contains secrets, conflicting arguments, unknown policy, provider-computed values presented as intent, or an unsupported resource schema.
- Stop if a module or `for_each` destination key is derived from mutable list order or does not exist under the current variables.
- Stop after apply if a normal plan remains non-empty. Correct configuration or explicitly schedule remediation before declaring the import complete.
- Stop if the team proposes `-lock=false`, direct state-file editing, unreviewed `state rm`, or a broad administrator credential to bypass a recoverable error.
decision
Freeze changes and build an authoritative import inventory
Select a small dependency-aware batch, pause manual and automated writers, and record each object's account, region, provider type, exact import identity, current settings, dependencies, owner, sensitivity, destination root, and proposed instance address.
Why this step matters
Import is safe only when operators know both sides of the mapping. Provider inventory establishes what exists; Terraform inventory establishes what is already managed. A change freeze prevents the remote object from changing between discovery, generation, plan, and apply. Starting with a small batch keeps the review comprehensible and limits the consequences of a wrong resource type, provider alias, or ownership boundary.
What to understand
Query the target through a read-only provider identity and record immutable ID, account, region, partition, relationships, policy, encryption, network exposure, lifecycle, and service owner. Screenshots are secondary evidence; prefer machine-readable API results stored securely.
Search every relevant backend, workspace, root repository, and controller for the provider ID. An object must not be imported because it is absent from one selected state while another root already owns it.
Choose an exact provider resource type. Some remote services are represented by multiple Terraform resources for configuration, policy, lifecycle, or attachments; one visible console object may require several reviewed imports.
Assign stable addresses before writing blocks. Use semantic `for_each` keys and complete module paths, not list positions or mutable display names that will later force state moves.
System changes
- Reads Terraform metadata and provider inventory; the organizational freeze pauses selected change paths but no state or remote object is modified.
Syntax explained
workspace show- Confirms the named state selection before comparing existing bindings.
state list- Lists current managed addresses without dumping secret-bearing state values.
terraform version && terraform workspace show && terraform state list && terraform providersTerraform v1.15.5
prod
module.network.aws_vpc.this
module.network.aws_subnet.private["eu-west-1a"]
Providers required by state:
provider[registry.terraform.io/hashicorp/aws]Checkpoint: Every object has one proven identity and destination
terraform state list | sortContinue whenThe batch manifest is uniquely keyed, dependencies and owners are recorded, and no target provider ID appears in any existing state or controller.
Stop whenAn identity is ambiguous, changes cannot be frozen, a duplicate owner exists, or the destination address depends on unstable ordering.
If this step fails
The provider reports that the import ID does not exist or is malformed.
Likely causeImport IDs are provider-resource-specific. The operator may have used a display name, ARN, URL, wrong account, wrong region, or an ID documented for a different resource type.
Verify caller identity and provider alias.Read the current provider resource import documentation.Query the object read-only through the provider-native API.Compare account, region, partition, and exact case with the inventory.
ResolutionCorrect the provider configuration or ID from authoritative inventory. Do not try random formats against production. Record the provider version and exact identity format because a later upgrade may add structured `identity` support or change validation.
Two Terraform addresses appear to represent the same remote object.
Likely causeThe object was imported twice, a composite resource overlaps another provider resource, or separate workspaces and roots claim the same identity. Future applies can fight or destroy shared infrastructure.
terraform state listterraform state show '<address-one>'terraform state show '<address-two>'Search all root inventories and backend keys for the provider ID.
ResolutionFreeze applies in every claimant, choose one authoritative owner and address, back up each state, and use official state commands under review to remove only the duplicate binding without deleting the remote object. Then plan every affected root and prove a single owner remains.
Security notes
- Inventory can expose topology, account IDs, policy, and sensitive metadata; store it with the migration record rather than in a public issue.
Alternatives
- Reduce the first batch to one representative object when the provider model or ownership is unfamiliar.
Stop conditions
- Do not infer an import ID from a console URL or resource name without current provider confirmation.
verification
Verify the target backend and preserve pre-import state
Initialize the exact target root, acquire its lock through a read-only plan, confirm workspace and addresses, pull a protected pre-import snapshot, and record the backend object version, state lineage, serial, commit, provider versions, and caller identity.
Why this step matters
An import writes a new state binding, so the target state must be known, locked, versioned, and recoverable before planning. An empty or wrong key makes existing configuration look absent and can turn a harmless import into mass creation. The pulled snapshot is sensitive but gives incident responders a known pre-change generation; object-version metadata and lineage prevent a later recovery from selecting the wrong workspace.
What to understand
Use short-lived backend and provider identities. Compare backend key, workspace, known addresses, state lineage, and serial with the protected environment inventory before writing a backup.
Write the pull directly into a protected path with restrictive permissions. Do not display it, pipe it through chat or logs, commit it, or attach it to an ordinary ticket. Record a checksum and secure object-version ID separately.
Confirm that native locking works and no plan, apply, drift, or maintenance job overlaps the migration. State locking cannot serialize manual cloud changes, which is why the external freeze remains necessary.
Test access to a retained version through an isolated recovery path before the window. A backup that depends on a disabled KMS key or undocumented role does not provide rollback.
System changes
- Writes local backend metadata and a protected read-only copy of the current state; no state binding or remote infrastructure changes.
Syntax explained
init -reconfigure- Re-reads the known backend configuration without intentionally migrating state.
umask 077- Restricts the created backup file to the current user on a Unix-like recovery workstation.
state pull- Downloads the current remote state snapshot; its output is raw sensitive state and must not enter logs.
Values stay on this page and are never sent or saved.
terraform init -reconfigure -input=false && terraform workspace show && terraform state list && umask 077 && terraform state pull > {{stateBackupPath}}Initializing the backend... Successfully configured the backend "s3"! prod module.network.aws_vpc.this Pre-import state captured in protected storage; serial 42 recorded in the migration manifest.
Checkpoint: The import can be stopped without losing the prior binding set
test -s {{stateBackupPath}} && terraform workspace show && terraform state listContinue whenA protected checksum-identified snapshot, state object version, lineage, serial, workspace, addresses, toolchain, and recovery owner are recorded.
Stop whenState is empty or unexpected, a lock is held, the backup is broadly readable, version recovery fails, or the caller cannot prove backend identity.
If this step fails
Terraform reports that the state lock is already held.
Likely causeAnother approved operation is active, an earlier process was interrupted, or an operator is pointing at the same state key from an unexpected checkout. A lock is evidence of serialization, not an inconvenience to bypass.
terraform workspace showterraform versionps -ef | grep '[t]erraform'Review the remote runner and change-system activity for the exact backend key.
ResolutionIdentify the lock owner and wait for the active operation. If the owner confirms that no process can still write, follow the backend-specific recovery procedure and use `terraform force-unlock` only with the exact lock ID, a protected state backup, and a second reviewer. Never set `-lock=false` to make progress.
A plan unexpectedly proposes many creates after backend initialization.
Likely causeThe configuration selected an empty state key, wrong workspace, wrong account, or wrong region. Terraform therefore sees configuration but no bindings to existing objects.
terraform workspace showterraform state listterraform providersCompare the backend key and state serial with the protected inventory.
ResolutionDo not apply. Restore the intended backend coordinates, reinitialize, and confirm the expected state addresses before planning again. If the original state is unavailable, treat recovery as an incident rather than attempting to recreate production from an empty state.
Terraform cannot decrypt or read the remote state object.
Likely causeThe runtime lost KMS permissions, the key was disabled or deleted, the object uses a different encryption context, or backend credentials select the wrong account.
terraform init -reconfigureVerify the caller identity through the cloud provider's identity command.Inspect KMS key status and backend audit logs without downloading state.Confirm the expected bucket and key from the change record.
ResolutionRestore the approved identity or recover the encryption key according to provider policy. Do not create an empty replacement state. If the key is irrecoverable, escalate to state and infrastructure recovery using a versioned snapshot and a complete remote-object inventory.
Security notes
- The backup is not a sanitized report. It has the same confidentiality and deletion requirements as the live state object.
Alternatives
- Use the backend or HCP Terraform's official version export workflow if it produces an equivalently protected identifiable snapshot.
Stop conditions
- Never continue with an import when pre-change state cannot be recovered and decrypted.
verification
Verify provider type, alias, and import identity read-only
Pin and initialize the provider, confirm the caller account and region, read the target object through the provider API, and consult the current provider resource import documentation for the exact `id` or supported `identity` shape.
Why this step matters
Terraform core supplies the import language, but the provider defines which resource types can import, how one object is identified, and which fields are readable or configurable. A bucket name, ARN, composite string, account-qualified identity, and regional identifier are not interchangeable. Verifying provider alias and caller context avoids importing a similarly named object from another account or region.
What to understand
Read the documentation for the exact pinned provider resource. Record the import identity format and whether structured `identity` is supported; `id` and `identity` are mutually exclusive in one block.
Query the object with a read-only native API and compare immutable identity, owner account, region, resource type, creation metadata, and dependencies. Avoid commands that mutate tags merely to prove access.
For aliased providers, make the alias explicit in the target resource or import block as supported and verify the root passes the intended configuration into a module.
Record fields the provider marks computed, optional, required, sensitive, deprecated, or replacement-triggering. This schema review drives manual HCL cleanup and later plan interpretation.
System changes
- Downloads or verifies pinned provider packages and reads the target object; no import or remote modification occurs.
Syntax explained
terraform providers- Shows the provider source and module ownership used to resolve the import.
identity- Uses provider-supported structured identity keys when available; it cannot be combined with `id`.
terraform providers && terraform validateProviders required by configuration: . └── provider[registry.terraform.io/hashicorp/aws] ~> 6.0 Success! The configuration is valid.
Checkpoint: The provider can resolve exactly one approved object
terraform providers && terraform validateContinue whenPinned provider, alias, caller identity, account, region, resource type, import ID format, and read-only object evidence all match the batch manifest.
Stop whenDocumentation is for another provider version, read access resolves another object, identity is ambiguous, or administrator credentials are proposed.
If this step fails
The import plan proposes changes or replacement in addition to import.
Likely causeThe authored or generated resource arguments do not match remote settings, immutable arguments differ, provider defaults were materialized incorrectly, or related resources are missing from the import scope.
terraform show import.tfplanterraform show -json import.tfplanRead the remote object through the provider-native API.Compare every configured, computed, optional, and force-new attribute.
ResolutionDo not apply. Adjust configuration to describe the approved remote object without changing it, model related resources explicitly, or split the migration. Continue only when the plan summary is imports with zero create, update, replace, and destroy actions.
Removing an import block causes confusion in future reviews.
Likely causeThe team treated the block only as temporary code and lost the historical mapping from provider identity to Terraform address, or retained it without documenting its idempotent historical purpose.
git log -- imports.tfterraform state listReview the migration record and post-import plan.Confirm the imported object is still bound at the intended address.
ResolutionEither retain the applied import block as an explicit historical record or remove it in a separate documented change after state and configuration converge. In both cases preserve the address-to-ID manifest, approval, plan, and verification evidence.
Security notes
- Do not paste provider credentials into a provider block. Identity verification should display only non-secret caller metadata.
Alternatives
- Postpone the resource when the current provider does not support import or cannot read enough state to manage it safely.
Stop conditions
- Do not experiment with identity strings against production until the exact format is documented and reviewed.
decision
Design stable destination addresses and dependency batches
Choose root or module ownership, semantic `for_each` keys, provider aliases, and import order. Write an address-to-provider-ID manifest and assert that every ID and address is unique before creating any block.
Why this step matters
An import address becomes durable state identity. A mutable list index, display name, or temporary module path can force state surgery or replacement later. Semantic keys make additions and removals reviewable. Dependency-aware batches also prevent a child object from being modeled before its parent, policy, attachment, or provider alias is understood.
What to understand
Prefer keys such as `audit` or a stable service identifier over `[0]`. Validate uniqueness of both map keys and provider IDs. Do not normalize two distinct provider names into the same key.
Use full module paths and instance keys. The destination resource must exist under current variables; an import block targeting an absent module instance cannot make the module call appear.
Separate resources whose provider schemas or owners require different review. Import a network and its policy attachments together only when the same team can verify the complete security effect.
Plan an explicit `moved` block if configuration must later shift from a root resource into a module. Import directly into the final stable address when the final module contract is already trustworthy.
System changes
- Evaluates local expressions and records the mapping; no state or provider object changes.
Syntax explained
for_each map key- Forms part of the persistent instance address and should represent stable identity rather than ordering.
module.<name>[<key>]- Selects a specific module instance that must already exist in current configuration.
terraform console> keys(local.existing_buckets) [ "audit", "logs", ] > local.existing_buckets["audit"].id "acme-prod-audit-logs"
Checkpoint: The mapping is unique, stable, and reviewer-readable
terraform consoleContinue whenEvery provider ID and complete destination address appears once, keys are semantic, module instances exist, and dependency order is documented.
Stop whenDuplicate IDs or addresses exist, a key derives from order, or reviewers cannot map the complete module address to immutable source.
If this step fails
A resource imported into a module plans to create another copy.
Likely causeThe `to` address omitted a module instance or `for_each` key, the module call resolved a different key set, or the resource inside the module has another label than assumed.
terraform state listterraform consoleterraform show import.tfplanInspect the selected immutable module release.
ResolutionDo not apply. Use the complete address such as `module.storage["prod"].aws_s3_bucket.this["logs"]`, prove that the module instance exists for current inputs, and import only after the plan shows one import at that exact address and no create.
The import succeeds but a following plan still shows a large diff.
Likely causeImport only created the state binding. The configuration still omits remote settings, provider normalization changed values, related objects need separate resources, or the desired policy genuinely differs.
terraform plan -out=post-import.tfplanterraform show post-import.tfplanterraform state show '<address>'Compare the provider API with configuration and approved intent.
ResolutionTreat the post-import diff as a new change proposal. Update configuration to encode accepted reality or schedule deliberate remediation. Do not claim that import preserves infrastructure unless the final normal plan is empty.
Security notes
- Do not include secret attributes in the manifest. The mapping needs identities and ownership, not raw state.
Alternatives
- Import one object at a direct root address and use a reviewed moved block later when the module is not yet stable.
Stop conditions
- Never import a shared object into two roots to make outputs easier to access.
config
Write owned resource and import blocks
Declare the resource at its final address with intentional security and lifecycle settings, then add a configuration-driven import block using the verified ID. Keep imports in a dedicated file or beside their resource according to the repository convention.
Why this step matters
Manual configuration is preferred when the team understands the provider model because it captures desired intent rather than every observed value. The import block makes the binding reviewable and participates in normal planning. Keeping resource and import mapping in code also avoids the unrepeatable context of a one-off `terraform import` shell command.
What to understand
Model all provider resources needed to preserve current security behavior. For many services, encryption, versioning, policy, lifecycle, networking, or attachments are separate resources and require separate identities and imports.
Do not add `ignore_changes` merely to force an empty plan. Use it only for a named external authority that legitimately owns a specific attribute, and document how security drift remains visible.
Add lifecycle protections such as `prevent_destroy` only when they match operating policy and understand that lifecycle rules are configuration controls, not a substitute for provider-native deletion protection and backups.
Retain the import block through plan and apply. After convergence, the repository may keep it as historical intent or remove it in a documented follow-up; both choices require migration evidence.
System changes
- Adds reviewed HCL source; planning reads the remote object but no state binding is written until the import plan is applied.
Syntax explained
to- Names the exact Terraform resource instance address that will own the remote object.
id- Supplies the provider-specific object identity, known during planning.
tags- Declares desired management metadata; if absent remotely it would be a change and must not be hidden inside the import window.
imports.tfValues stay on this page and are never sent or saved.
resource "aws_s3_bucket" "existing" {
bucket = "{{resourceId}}"
tags = {
ManagedBy = "terraform"
Owner = "platform"
}
}
import {
to = aws_s3_bucket.existing
id = "{{resourceId}}"
}Success! The configuration is valid. aws_s3_bucket.existing: Preparing import... [id=acme-prod-audit-logs] aws_s3_bucket.existing: Refreshing state... [id=acme-prod-audit-logs]
Checkpoint: Configuration expresses current reality and future ownership
terraform fmt -check -recursive && terraform validate && terraform plan -out=import.tfplan -lock-timeout=5mContinue whenValidation passes and the plan prepares exactly the reviewed object at the final address without any create, update, replace, or destroy.
Stop whenThe destination is absent, related controls are unmodeled, or the plan includes a remote change.
If this step fails
Generated configuration contains conflicting or invalid arguments.
Likely causeThe experimental generator emitted provider-observed values for mutually exclusive, computed-only, deprecated, or context-dependent schema fields. Generated HCL is a best-effort template, not authoritative desired configuration.
terraform validateterraform plan -generate-config-out=generated_resources.tfRead the provider schema and current resource documentation.Compare generated arguments with the remote API response.
ResolutionKeep the failed output as a protected working artifact, remove computed-only or conflicting arguments, choose one valid mutually exclusive form, make desired behavior explicit, and rerun validate and plan. Never apply generated configuration without manual ownership and review.
An imported object is immediately planned for deletion.
Likely causeIts resource block is absent, conditionally disabled, keyed differently by `for_each`, or located in a module instance that the current variables do not instantiate.
terraform state listterraform show import.tfplanterraform consoleReview count, for_each keys, module instances, and variable files.
ResolutionStop before apply. Restore the destination configuration and stable instance key, or correct the import address. If the object should not be managed, remove only the proposed import block before it enters state; do not import and then use destroy as cleanup.
The generated file path already exists and Terraform refuses to continue.
Likely causeThe `-generate-config-out` option intentionally requires a new path so it cannot overwrite reviewed configuration. A prior attempt or checked-in generated file occupies the target.
test -e generated_resources.tfgit status --shortReview and preserve any prior generated artifact securely.Choose a new unique working path.
ResolutionDo not delete a reviewed file blindly. Compare and archive the prior artifact, choose a new ignored path for the next generation attempt, and manually merge only validated resource blocks into owned source files.
Security notes
- Never place an observed access key, password, private material, or provider token into HCL just because it is required by a generated schema.
Alternatives
- Use generated configuration in the next step only when manual authoring cannot yet cover a small well-understood resource.
Stop conditions
- Do not interpret an import block as permission to normalize or harden the object during the same apply.
config
Optionally generate HCL as an experimental working draft
When no resource block exists, place only the reviewed import block, run plan with `-generate-config-out` to a new ignored path, and inspect the result as a provider-derived draft. Never overwrite source or apply it unchanged.
Why this step matters
Terraform 1.15 can ask the provider schema for observed values and write a candidate resource block, which is useful for discovery but remains experimental in official documentation. Providers can return computed fields, zero values, mutually exclusive settings, defaults that should stay implicit, or representations that fail validation. The generator cannot know organizational intent, ownership, acceptable risk, or which related resources belong together.
What to understand
The output path must not already exist. This guard prevents Terraform from overwriting reviewed HCL. Keep generated files ignored and protected until a human decides which content becomes owned source.
Read every argument against the current provider resource schema. Remove computed-only values, select one valid option from mutually exclusive fields, replace observed defaults with explicit policy where necessary, and add descriptions or module inputs.
Inspect for secrets and sensitive metadata before review. Do not upload generated HCL to public code-review tools if it contains internal identifiers, network topology, policies, or values returned by the provider.
Move the cleaned resource block into the repository's standard files, format and validate, delete or securely archive the working draft, then run generation no further. The owned HCL must stand on its own.
System changes
- Reads the target through the provider and writes a new local HCL working file; it does not bind state or change the remote object.
Syntax explained
-generate-config-out={{generatedPath}}- Writes experimental provider-derived configuration to a new path only for import targets that lack resource blocks.
test ! -e- Fails before Terraform when the intended path exists, protecting prior or reviewed content.
Values stay on this page and are never sent or saved.
test ! -e {{generatedPath}} && terraform plan -generate-config-out={{generatedPath}} -lock-timeout=5maws_s3_bucket.existing: Preparing import... [id=acme-prod-audit-logs] aws_s3_bucket.existing: Refreshing state... [id=acme-prod-audit-logs] Terraform has generated configuration and written it to .migration/generated-storage-01.tf. Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
Checkpoint: Generated HCL has become intentional owned configuration
terraform fmt -check -recursive && terraform validate && git diff -- '*.tf'Continue whenOnly reviewed configurable arguments enter owned HCL, secrets and computed fields are absent, conflicts are resolved, and the diff documents security and lifecycle intent.
Stop whenGeneration reports conflicting arguments, the file includes an unexplained value, a secret appears, or reviewers propose applying the raw draft.
If this step fails
A bulk `for_each` import maps an object to the wrong key.
Likely causeThe inventory map uses unstable list positions, duplicate names, normalized keys, or a field that does not uniquely identify the provider object. Address keys are persistent state identity, not presentation order.
terraform consoleterraform show import.tfplanCompare each `each.key`, `each.value.id`, and destination address.Check the inventory for duplicate IDs and keys.
ResolutionCancel the plan, replace positional or mutable keys with stable reviewed keys, assert uniqueness, and generate a machine-readable address-to-ID manifest. Import in smaller batches when a reviewer cannot confidently inspect the entire mapping.
Import cannot refresh the object because provider read access is denied.
Likely causeThe migration role can access state but lacks least-privilege read permissions for the remote object, uses a wrong provider alias, or encounters an organization policy boundary.
terraform providersVerify caller identity without printing credentials.Read provider audit-denial events.Query only the target object through the provider-native read API.
ResolutionGrant a bounded temporary read or import permission through the approved identity process, correct the provider alias, and retry the plan. Do not grant broad administrator rights or copy a static credential into provider configuration.
Security notes
- Generated files and error messages can include sensitive provider observations. Protect both stdout and the file as migration artifacts.
Alternatives
- Author the resource manually and use `terraform state show` only after import for protected diagnostic comparison.
Stop conditions
- Never use generated configuration to assert desired policy without a named human owner for every significant argument.
config
Use `for_each` only for a reviewable stable batch
For similar objects, define a stable map of semantic keys to verified IDs and destinations, then let the import and resource blocks share the same key. Generate a manifest and review every mapping before plan.
Why this step matters
Current import blocks support `for_each`, making repeated mappings reviewable without copying blocks. The convenience increases risk when a map is built from unstable order, non-unique names, or an oversized discovery result. The same stable key must select the resource instance and its provider ID so reviewers can trace one line to one address. A small batch keeps the plan human-auditable.
What to understand
Use a literal or known map whose keys are stable business or infrastructure identities. Values and keys must be known during planning; do not derive import identity from a remote object Terraform has not yet read.
Assert uniqueness outside Terraform or through validated input. A duplicate ID under two keys can attempt to bind one object twice, while a duplicate normalized key can silently drop inventory before HCL is produced.
Split batches by provider alias, security boundary, dependency, and owner. A concise loop is not a reason to combine hundreds of objects under one state lock and approval.
Render the complete address-to-ID table from plan JSON and compare it to the signed manifest. Sample checking is not enough because one wrong key can create lasting ownership conflict.
System changes
- Adds batch mapping configuration; planning reads each target but apply remains the point where bindings are written.
Syntax explained
for_each = local.existing_buckets- Creates one import action and resource instance per stable reviewed map key.
each.key- Forms the persistent Terraform address key and must not change when display order changes.
each.value.id- Supplies the exact provider object identity associated with that stable key.
imports-storage.tflocals {
existing_buckets = {
audit = { id = "acme-prod-audit-logs" }
logs = { id = "acme-prod-service-logs" }
}
}
resource "aws_s3_bucket" "existing" {
for_each = local.existing_buckets
bucket = each.value.id
}
import {
for_each = local.existing_buckets
to = aws_s3_bucket.existing[each.key]
id = each.value.id
}aws_s3_bucket.existing["audit"]: Preparing import... [id=acme-prod-audit-logs] aws_s3_bucket.existing["logs"]: Preparing import... [id=acme-prod-service-logs] Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.
Checkpoint: Batch mapping is complete and one-to-one
terraform plan -out=import.tfplan -lock-timeout=5m && terraform show -json import.tfplan | jq -r '.resource_changes[] | select(.change.importing != null) | .address'Continue whenThe address list exactly matches the approved manifest, each ID appears once, and the plan has imports only.
Stop whenAny key is positional or duplicated, an ID maps twice, the batch spans unrelated owners, or the plan cannot be fully reviewed.
If this step fails
A resource imported into a module plans to create another copy.
Likely causeThe `to` address omitted a module instance or `for_each` key, the module call resolved a different key set, or the resource inside the module has another label than assumed.
terraform state listterraform consoleterraform show import.tfplanInspect the selected immutable module release.
ResolutionDo not apply. Use the complete address such as `module.storage["prod"].aws_s3_bucket.this["logs"]`, prove that the module instance exists for current inputs, and import only after the plan shows one import at that exact address and no create.
Security notes
- Maps should contain identifiers only. Do not embed credentials or full provider API responses in locals.
Alternatives
- Write one import block per object when explicit repetition makes a high-risk mapping easier to review.
Stop conditions
- Do not import a bulk search result merely because Terraform can enumerate it.
verification
Require an import-only saved plan
Create a normal refresh-backed saved plan under the state lock. Review the human summary and require only the approved imports with zero add, change, replace, and destroy. In plan JSON, identify imports through `change.importing`; the accompanying `change.actions` values must remain only `no-op` or `read`.
Why this step matters
The plan is the first point where Terraform combines configuration, existing state, provider identity, and remote observations. An import action can coexist with updates or replacement when HCL differs, so the summary and full change actions must be inspected. Saving the plan ties the approved binding to the exact state and configuration. JSON supports deterministic gating, but provider warnings and human-readable attribute differences still require expert review.
What to understand
Confirm the plan was produced from the approved commit, variables, provider versions, identity, backend, and state serial. If any change occurs before apply, discard and regenerate it.
Review every import target and provider ID. Inspect sensitive, immutable, policy, network, encryption, lifecycle, and attachment fields even when the action summary is clean.
Fail the pipeline if any resource action contains create, update, delete, replace, or forget in this migration window. Move intended hardening into a separate post-import change with its own outage and rollback analysis.
Protect both binary and JSON plan artifacts. The JSON rendering can expose values redacted in terminal output and should have short retention tied to the migration approval.
System changes
- Reads remote objects and state under a lock and writes protected saved plan artifacts; no binding is committed until apply.
Syntax explained
-out=import.tfplan- Captures the exact import proposal that reviewers approve and apply.
show -json- Produces a machine-readable plan for exact action and address checks; its content is sensitive.
Values stay on this page and are never sent or saved.
terraform plan -out=import.tfplan -lock-timeout=5m -input=false && terraform show import.tfplan && terraform show -json import.tfplan > /secure/{{importBatch}}-plan.jsonTerraform will perform the following actions:
# aws_s3_bucket.existing will be imported
resource "aws_s3_bucket" "existing" {
id = "acme-prod-audit-logs"
}
Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.Checkpoint: Nothing will be recreated or modified during adoption
terraform show -json import.tfplan | jq -e '([.resource_changes[] | select(.change.importing != null)] | length) > 0 and ([.resource_changes[].change.actions[]] | all(. == "no-op" or . == "read"))'Continue whenThe gate passes, summary is exactly the reviewed import count with 0 add, 0 change, 0 destroy, and each identity-to-address mapping is approved.
Stop whenAny non-import managed action appears, provider warnings are unresolved, the plan is stale, or an artifact is exposed.
If this step fails
Generated configuration contains conflicting or invalid arguments.
Likely causeThe experimental generator emitted provider-observed values for mutually exclusive, computed-only, deprecated, or context-dependent schema fields. Generated HCL is a best-effort template, not authoritative desired configuration.
terraform validateterraform plan -generate-config-out=generated_resources.tfRead the provider schema and current resource documentation.Compare generated arguments with the remote API response.
ResolutionKeep the failed output as a protected working artifact, remove computed-only or conflicting arguments, choose one valid mutually exclusive form, make desired behavior explicit, and rerun validate and plan. Never apply generated configuration without manual ownership and review.
The generated file path already exists and Terraform refuses to continue.
Likely causeThe `-generate-config-out` option intentionally requires a new path so it cannot overwrite reviewed configuration. A prior attempt or checked-in generated file occupies the target.
test -e generated_resources.tfgit status --shortReview and preserve any prior generated artifact securely.Choose a new unique working path.
ResolutionDo not delete a reviewed file blindly. Compare and archive the prior artifact, choose a new ignored path for the next generation attempt, and manually merge only validated resource blocks into owned source files.
Security notes
- Store plan JSON in a protected temporary location and delete it according to evidence policy after preserving a sanitized action manifest.
Alternatives
- Split the batch until the import-only plan is small enough for complete human review.
Stop conditions
- Never approve based only on `Plan: N to import`; inspect every action and significant attribute.
command
Apply the exact import plan and verify each binding
Reconfirm the freeze, lock, commit, state serial, artifact digest, provider identity, and approval, then apply the saved import plan. List and show only targeted state addresses and verify remote objects remain unchanged.
Why this step matters
Applying the saved artifact writes bindings and a new remote state version. It should not recreate the object, but a partial apply can import some addresses before a provider or permission error stops the batch. Operators must inspect resulting state rather than rerun blindly. Service-level and provider-native evidence proves that availability, policy, identifiers, and timestamps did not change while Terraform adopted ownership.
What to understand
Verify the saved-plan digest and state context immediately before apply. A changed state invalidates the artifact; another manual object update invalidates the acceptance evidence even if the plan can still execute.
Watch the apply summary and backend object version. For a partial batch, compare every intended address with state and the provider inventory, then create a new plan for only missing imports.
Use `state show` sparingly because it may display sensitive values. Capture a sanitized identity and address manifest rather than the complete output.
Run provider-native read-only service checks and compare immutable identity, creation time, policy, encryption, network, tags, and dependencies to the pre-import inventory.
System changes
- Writes the reviewed resource bindings and a new versioned remote state snapshot; the accepted plan performs no remote create, update, replacement, or deletion.
Syntax explained
import.tfplan- Applies the exact reviewed import-only proposal instead of recalculating.
state show '{{resourceAddress}}'- Confirms the imported address and provider identity; output may contain sensitive attributes.
Values stay on this page and are never sent or saved.
terraform apply -lock-timeout=5m -input=false import.tfplan && terraform state list && terraform state show '{{resourceAddress}}'aws_s3_bucket.existing: Importing... [id=acme-prod-audit-logs] aws_s3_bucket.existing: Import complete [id=acme-prod-audit-logs] Apply complete! Resources: 1 imported, 0 added, 0 changed, 0 destroyed. aws_s3_bucket.existing
Checkpoint: Each existing object is bound once and remained unchanged
terraform state list | sort && terraform state show '{{resourceAddress}}'Continue whenEvery approved address exists once, maps to the exact provider ID, apply reports imports only, and provider audit evidence contains no mutation.
Stop whenApply is partial or reports a change, the wrong ID appears, duplicate ownership is discovered, or service policy or availability differs.
If this step fails
Terraform says the resource address already exists in state.
Likely causeThe remote object may already be managed at that address, a previous import succeeded, or the operator is using a different checkout against the same backend. Re-importing would violate the one-address-to-one-object assumption.
terraform state listterraform state show 'aws_s3_bucket.existing'terraform plan -refresh-onlyCompare the state object identity with the approved import inventory.
ResolutionDo not remove or overwrite the binding. Confirm whether state already points to the intended object. If the object belongs at another address, design a `moved` block or reviewed state migration; if the inventory is stale, update it and continue without another import.
A bulk `for_each` import maps an object to the wrong key.
Likely causeThe inventory map uses unstable list positions, duplicate names, normalized keys, or a field that does not uniquely identify the provider object. Address keys are persistent state identity, not presentation order.
terraform consoleterraform show import.tfplanCompare each `each.key`, `each.value.id`, and destination address.Check the inventory for duplicate IDs and keys.
ResolutionCancel the plan, replace positional or mutable keys with stable reviewed keys, assert uniqueness, and generate a machine-readable address-to-ID manifest. Import in smaller batches when a reviewer cannot confidently inspect the entire mapping.
Security notes
- Sanitize state-show evidence. The imported object may expose policy, endpoints, account identifiers, and secret-bearing provider fields.
Alternatives
- Apply one import at a time when the provider has poor partial-failure semantics or each object has a different owner.
Stop conditions
- Do not rerun an import block after partial apply until current state proves which bindings already succeeded.
verification
Require an empty normal plan after import
Run a fresh normal plan against the imported state. Compare configuration, provider observations, and actual policy; correct owned HCL until the plan is empty without using broad ignore rules or changing infrastructure inside the import change.
Why this step matters
Import establishes the state binding but does not prove that HCL represents the remote object. A fresh normal plan is the convergence test. If it proposes change, the team must decide whether configuration should adopt approved reality or a later remediation should restore intended policy. Applying a refresh-only state update cannot permanently resolve a difference while configuration still requests another value.
What to understand
Inspect every diff and classify it as missing desired configuration, provider normalization, computed field, external authority, intentional drift, or unsafe legacy setting. Assign an owner and evidence.
To accept an approved remote setting, update HCL and replan. To restore intended policy, leave HCL authoritative but schedule a separate normal plan with service impact and rollback. Do not mix remediation into import approval.
Use `ignore_changes` only for specific attributes owned by another named system, with independent drift monitoring. Never ignore complete resources, security groups, policies, encryption, or public exposure to make a plan quiet.
Require exit code 0 after corrections. Store a sanitized diff summary, final state object version, configuration commit, and provider verification in the migration record.
System changes
- Reads state and provider APIs and writes a protected saved plan; configuration corrections change source only until a later separately approved apply.
Syntax explained
-detailed-exitcode- Returns 0 for true convergence, 1 for error, and 2 for remaining differences.
-out=post-import.tfplan- Preserves the observed post-import proposal for protected diagnosis; do not apply it automatically.
terraform plan -out=post-import.tfplan -lock-timeout=5m -detailed-exitcodeaws_s3_bucket.existing: Refreshing state... [id=acme-prod-audit-logs] No changes. Your infrastructure matches the configuration.
Checkpoint: Terraform can manage the object without surprise
terraform plan -detailed-exitcode -lock-timeout=5mContinue whenThe command exits 0, configuration describes approved behavior, and no broad ignore rule hides security or ownership drift.
Stop whenExit code is 1 or 2, immutable replacement appears, generated defaults remain unexplained, or security differences are ignored.
If this step fails
Generated configuration contains conflicting or invalid arguments.
Likely causeThe experimental generator emitted provider-observed values for mutually exclusive, computed-only, deprecated, or context-dependent schema fields. Generated HCL is a best-effort template, not authoritative desired configuration.
terraform validateterraform plan -generate-config-out=generated_resources.tfRead the provider schema and current resource documentation.Compare generated arguments with the remote API response.
ResolutionKeep the failed output as a protected working artifact, remove computed-only or conflicting arguments, choose one valid mutually exclusive form, make desired behavior explicit, and rerun validate and plan. Never apply generated configuration without manual ownership and review.
The generated file path already exists and Terraform refuses to continue.
Likely causeThe `-generate-config-out` option intentionally requires a new path so it cannot overwrite reviewed configuration. A prior attempt or checked-in generated file occupies the target.
test -e generated_resources.tfgit status --shortReview and preserve any prior generated artifact securely.Choose a new unique working path.
ResolutionDo not delete a reviewed file blindly. Compare and archive the prior artifact, choose a new ignored path for the next generation attempt, and manually merge only validated resource blocks into owned source files.
Security notes
- An empty plan is not a security assessment. Verify service controls independently and schedule legacy-risk remediation.
Alternatives
- Leave a documented non-empty remediation plan unapplied until the service owner approves a separate window; the import itself remains incomplete for operational handoff.
Stop conditions
- Do not label the migration complete while a normal plan proposes any unexplained action.
verification
Prove single ownership and least-privilege denial
Search all state inventories for the imported IDs, run a plan from the one authoritative root, and verify an unapproved identity cannot read or write the protected state or modify the object through the Terraform workflow.
Why this step matters
A successful import can still create conflict when another root, workspace, or controller claims the object later. Single ownership must be demonstrated across the estate, not assumed from one state. A negative identity test verifies that centralizing configuration did not accidentally grant every developer read access to secret-bearing state or production write access.
What to understand
Query the organization's state catalog or protected manifests for each provider ID. If no catalog exists, search root repositories and backend inventories under controlled access and create one as a migration outcome.
Verify the authoritative root owns the complete lifecycle, while downstream consumers receive only narrow published outputs. They should not import the same object or read full state merely to get one identifier.
Test denied access with a purpose-built identity from an isolated runner. Do not intentionally trigger broad cloud-provider mutation denial; state read and plan authorization evidence is sufficient.
Document other controllers that remain authoritative for specific ignored attributes. Ensure their ownership cannot replace, delete, or duplicate the Terraform-managed object unexpectedly.
System changes
- Reads state catalogs and provider APIs; the negative test attempts only denied state access and does not change the remote object.
Syntax explained
-detailed-exitcode- Provides deterministic convergence evidence for the authoritative root.
AccessDenied- Expected result for an identity outside the state and apply authorization boundary.
terraform state list && terraform plan -detailed-exitcode -lock-timeout=5mNo changes. Your infrastructure matches the configuration. Unauthorized runner: Error acquiring state: AccessDenied Authorized root: exit code 0
Checkpoint: Exactly one root and approved role own the object
terraform plan -detailed-exitcode -lock-timeout=5mContinue whenThe authoritative root exits 0, no other state manifest contains the provider ID, and the denied identity cannot read state or initiate a production plan.
Stop whenA duplicate claim exists, broad state read succeeds, or another controller can replace or delete the imported object without coordination.
If this step fails
Two Terraform addresses appear to represent the same remote object.
Likely causeThe object was imported twice, a composite resource overlaps another provider resource, or separate workspaces and roots claim the same identity. Future applies can fight or destroy shared infrastructure.
terraform state listterraform state show '<address-one>'terraform state show '<address-two>'Search all root inventories and backend keys for the provider ID.
ResolutionFreeze applies in every claimant, choose one authoritative owner and address, back up each state, and use official state commands under review to remove only the duplicate binding without deleting the remote object. Then plan every affected root and prove a single owner remains.
Security notes
- Use an isolated denied identity and keep denial logs protected; account and backend paths are still operational metadata.
Alternatives
- Use policy simulation when the provider prohibits a safe live denial test, then validate real role boundaries during the next scheduled control audit.
Stop conditions
- Freeze every claimant immediately if the same provider ID appears in two states.
instruction
Commit the migration record and establish ongoing ownership
Commit owned configuration, provider locks, import-history decision, sanitized mapping, verification, owner, review date, and rollback instructions. Resume changes only after monitoring and an empty normal plan are recorded.
Why this step matters
Adoption transfers operational responsibility, not only code. Future operators need to know why the object exists, who owns it, which provider identity was imported, which legacy risks remain, how drift is reviewed, and how to release the binding without deleting the service. A 90-day review is appropriate because provider schemas and legacy imported settings can change faster than mature greenfield configuration.
What to understand
Commit resource and import blocks according to convention, `.terraform.lock.hcl`, module pins, tests, and documentation. Never commit state, plans, generated working files, provider credentials, or raw API inventory.
Preserve a sanitized address-to-ID manifest, import-only plan action summary, final empty-plan evidence, state object version IDs, reviewers, and service-level checks in the protected change record.
Decide explicitly whether import blocks remain. Retention makes historical mapping visible; removal can reduce one-time migration code. Either way, state and configuration remain authoritative and the decision is documented.
Enable scheduled plans and service monitoring, assign drift and provider-upgrade owners, and define release-without-destroy. Resume manual changes only through the new Terraform ownership or an emergency process that immediately reconciles configuration.
System changes
- Finalizes source and operational records, then lifts the migration freeze; no additional remote changes occur.
Syntax explained
fmt -check -recursive- Ensures the committed HCL, including import history, follows canonical formatting.
validate- Confirms the final owned configuration remains structurally valid.
terraform fmt -check -recursive && terraform validate && terraform plan -detailed-exitcode -lock-timeout=5mSuccess! The configuration is valid. No changes. Your infrastructure matches the configuration. Migration storage-2026-07-batch-01 accepted; next review 2026-10-22.
Checkpoint: The imported object has a sustainable lifecycle
terraform validate && terraform plan -detailed-exitcode -lock-timeout=5mContinue whenValidation passes, plan exits 0, owner and monitoring are active, import evidence is protected, and rollback and release-without-destroy are documented.
Stop whenNo owner accepts the object, residual drift exists, monitoring is absent, or raw state or generated HCL would enter source control.
If this step fails
Removing an import block causes confusion in future reviews.
Likely causeThe team treated the block only as temporary code and lost the historical mapping from provider identity to Terraform address, or retained it without documenting its idempotent historical purpose.
git log -- imports.tfterraform state listReview the migration record and post-import plan.Confirm the imported object is still bound at the intended address.
ResolutionEither retain the applied import block as an explicit historical record or remove it in a separate documented change after state and configuration converge. In both cases preserve the address-to-ID manifest, approval, plan, and verification evidence.
A plan or state pull exposes a secret in a terminal, artifact, or log.
Likely causeState and plan files contain provider-returned attributes, and the `sensitive` marker controls display rather than guaranteeing omission from state. Debug logging or unredacted artifacts expanded the exposure.
Stop log and artifact publication.Identify every principal and system that received the data.Review whether the value is sensitive, ephemeral, or write-only in the configuration.Preserve only access-controlled forensic evidence.
ResolutionTreat the value as compromised, rotate it at its authoritative system, revoke exposed artifacts, restrict state access, and redesign the resource to use ephemeral or write-only provider features where supported. Do not try to erase evidence by hand-editing state.
Security notes
- Sanitize migration documentation; retain sensitive state, plans, and provider responses only in approved restricted storage.
Alternatives
- Keep the change freeze longer and operate in observation mode when the team cannot yet guarantee drift ownership or service rollback.
Stop conditions
- Do not resume out-of-band changes until emergency reconciliation and authority rules are published.
Finish line
Verification checklist
terraform show -json import.tfplan | jq -e '([.resource_changes[] | select(.change.importing != null)] | length) > 0 and ([.resource_changes[].change.actions[]] | all(. == "no-op" or . == "read"))'At least one reviewed resource contains `change.importing`, and all accompanying action values are `no-op` or `read`, with zero create, update, replacement, forget, or destroy.terraform plan -detailed-exitcode -lock-timeout=5mThe final normal plan exits 0 after configuration accurately describes approved remote behavior.terraform state list && terraform state show '{{resourceAddress}}'Each target address appears once and maps to the exact provider ID in the signed import manifest.test -s {{stateBackupPath}}The protected pre-import snapshot, object version, lineage, serial, checksum, and recovery owner are recorded without exposing state.Recovery guidance
Common problems and safe checks
Terraform reports that the state lock is already held.
Likely causeAnother approved operation is active, an earlier process was interrupted, or an operator is pointing at the same state key from an unexpected checkout. A lock is evidence of serialization, not an inconvenience to bypass.
terraform workspace showterraform versionps -ef | grep '[t]erraform'Review the remote runner and change-system activity for the exact backend key.
ResolutionIdentify the lock owner and wait for the active operation. If the owner confirms that no process can still write, follow the backend-specific recovery procedure and use `terraform force-unlock` only with the exact lock ID, a protected state backup, and a second reviewer. Never set `-lock=false` to make progress.
Initialization asks to migrate state when only a backend setting was expected to change.
Likely causeTerraform detected a different backend identity, key, workspace prefix, or partial configuration than the checkout previously initialized. Accepting the prompt without understanding the destination can split or overwrite state.
terraform versiongit diff -- '*.tf' '*.hcl'Inspect the non-secret backend configuration supplied by the approved runner.List versioned state objects in the intended backend through the provider console.
ResolutionStop, compare the old and new backend coordinates, pull a protected backup from the known-good backend, and choose `-reconfigure` only for the same state location or `-migrate-state` only for a deliberate reviewed migration. Do not guess at an interactive prompt in production.
A plan unexpectedly proposes many creates after backend initialization.
Likely causeThe configuration selected an empty state key, wrong workspace, wrong account, or wrong region. Terraform therefore sees configuration but no bindings to existing objects.
terraform workspace showterraform state listterraform providersCompare the backend key and state serial with the protected inventory.
ResolutionDo not apply. Restore the intended backend coordinates, reinitialize, and confirm the expected state addresses before planning again. If the original state is unavailable, treat recovery as an incident rather than attempting to recreate production from an empty state.
A plan or state pull exposes a secret in a terminal, artifact, or log.
Likely causeState and plan files contain provider-returned attributes, and the `sensitive` marker controls display rather than guaranteeing omission from state. Debug logging or unredacted artifacts expanded the exposure.
Stop log and artifact publication.Identify every principal and system that received the data.Review whether the value is sensitive, ephemeral, or write-only in the configuration.Preserve only access-controlled forensic evidence.
ResolutionTreat the value as compromised, rotate it at its authoritative system, revoke exposed artifacts, restrict state access, and redesign the resource to use ephemeral or write-only provider features where supported. Do not try to erase evidence by hand-editing state.
Terraform cannot decrypt or read the remote state object.
Likely causeThe runtime lost KMS permissions, the key was disabled or deleted, the object uses a different encryption context, or backend credentials select the wrong account.
terraform init -reconfigureVerify the caller identity through the cloud provider's identity command.Inspect KMS key status and backend audit logs without downloading state.Confirm the expected bucket and key from the change record.
ResolutionRestore the approved identity or recover the encryption key according to provider policy. Do not create an empty replacement state. If the key is irrecoverable, escalate to state and infrastructure recovery using a versioned snapshot and a complete remote-object inventory.
A CI plan waits indefinitely or times out acquiring a lock.
Likely causeRuns are not serialized at the pipeline layer, the lock timeout is shorter than normal applies, or an abandoned operation still owns the backend lock.
Inspect queued and running jobs for the same state key.Compare lock metadata with the runner identity and start time.Review recent apply duration and cancellation events.Confirm that no scheduled drift job overlaps the change window.
ResolutionSerialize operations per backend key, use a bounded `-lock-timeout` that matches operating expectations, cancel duplicate queued work, and investigate stale locks explicitly. A timeout should fail closed and alert; it must not retry with locking disabled.
Terraform says the resource address already exists in state.
Likely causeThe remote object may already be managed at that address, a previous import succeeded, or the operator is using a different checkout against the same backend. Re-importing would violate the one-address-to-one-object assumption.
terraform state listterraform state show 'aws_s3_bucket.existing'terraform plan -refresh-onlyCompare the state object identity with the approved import inventory.
ResolutionDo not remove or overwrite the binding. Confirm whether state already points to the intended object. If the object belongs at another address, design a `moved` block or reviewed state migration; if the inventory is stale, update it and continue without another import.
The provider reports that the import ID does not exist or is malformed.
Likely causeImport IDs are provider-resource-specific. The operator may have used a display name, ARN, URL, wrong account, wrong region, or an ID documented for a different resource type.
Verify caller identity and provider alias.Read the current provider resource import documentation.Query the object read-only through the provider-native API.Compare account, region, partition, and exact case with the inventory.
ResolutionCorrect the provider configuration or ID from authoritative inventory. Do not try random formats against production. Record the provider version and exact identity format because a later upgrade may add structured `identity` support or change validation.
The import plan proposes changes or replacement in addition to import.
Likely causeThe authored or generated resource arguments do not match remote settings, immutable arguments differ, provider defaults were materialized incorrectly, or related resources are missing from the import scope.
terraform show import.tfplanterraform show -json import.tfplanRead the remote object through the provider-native API.Compare every configured, computed, optional, and force-new attribute.
ResolutionDo not apply. Adjust configuration to describe the approved remote object without changing it, model related resources explicitly, or split the migration. Continue only when the plan summary is imports with zero create, update, replace, and destroy actions.
Generated configuration contains conflicting or invalid arguments.
Likely causeThe experimental generator emitted provider-observed values for mutually exclusive, computed-only, deprecated, or context-dependent schema fields. Generated HCL is a best-effort template, not authoritative desired configuration.
terraform validateterraform plan -generate-config-out=generated_resources.tfRead the provider schema and current resource documentation.Compare generated arguments with the remote API response.
ResolutionKeep the failed output as a protected working artifact, remove computed-only or conflicting arguments, choose one valid mutually exclusive form, make desired behavior explicit, and rerun validate and plan. Never apply generated configuration without manual ownership and review.
An imported object is immediately planned for deletion.
Likely causeIts resource block is absent, conditionally disabled, keyed differently by `for_each`, or located in a module instance that the current variables do not instantiate.
terraform state listterraform show import.tfplanterraform consoleReview count, for_each keys, module instances, and variable files.
ResolutionStop before apply. Restore the destination configuration and stable instance key, or correct the import address. If the object should not be managed, remove only the proposed import block before it enters state; do not import and then use destroy as cleanup.
Two Terraform addresses appear to represent the same remote object.
Likely causeThe object was imported twice, a composite resource overlaps another provider resource, or separate workspaces and roots claim the same identity. Future applies can fight or destroy shared infrastructure.
terraform state listterraform state show '<address-one>'terraform state show '<address-two>'Search all root inventories and backend keys for the provider ID.
ResolutionFreeze applies in every claimant, choose one authoritative owner and address, back up each state, and use official state commands under review to remove only the duplicate binding without deleting the remote object. Then plan every affected root and prove a single owner remains.
A bulk `for_each` import maps an object to the wrong key.
Likely causeThe inventory map uses unstable list positions, duplicate names, normalized keys, or a field that does not uniquely identify the provider object. Address keys are persistent state identity, not presentation order.
terraform consoleterraform show import.tfplanCompare each `each.key`, `each.value.id`, and destination address.Check the inventory for duplicate IDs and keys.
ResolutionCancel the plan, replace positional or mutable keys with stable reviewed keys, assert uniqueness, and generate a machine-readable address-to-ID manifest. Import in smaller batches when a reviewer cannot confidently inspect the entire mapping.
A resource imported into a module plans to create another copy.
Likely causeThe `to` address omitted a module instance or `for_each` key, the module call resolved a different key set, or the resource inside the module has another label than assumed.
terraform state listterraform consoleterraform show import.tfplanInspect the selected immutable module release.
ResolutionDo not apply. Use the complete address such as `module.storage["prod"].aws_s3_bucket.this["logs"]`, prove that the module instance exists for current inputs, and import only after the plan shows one import at that exact address and no create.
The import succeeds but a following plan still shows a large diff.
Likely causeImport only created the state binding. The configuration still omits remote settings, provider normalization changed values, related objects need separate resources, or the desired policy genuinely differs.
terraform plan -out=post-import.tfplanterraform show post-import.tfplanterraform state show '<address>'Compare the provider API with configuration and approved intent.
ResolutionTreat the post-import diff as a new change proposal. Update configuration to encode accepted reality or schedule deliberate remediation. Do not claim that import preserves infrastructure unless the final normal plan is empty.
The generated file path already exists and Terraform refuses to continue.
Likely causeThe `-generate-config-out` option intentionally requires a new path so it cannot overwrite reviewed configuration. A prior attempt or checked-in generated file occupies the target.
test -e generated_resources.tfgit status --shortReview and preserve any prior generated artifact securely.Choose a new unique working path.
ResolutionDo not delete a reviewed file blindly. Compare and archive the prior artifact, choose a new ignored path for the next generation attempt, and manually merge only validated resource blocks into owned source files.
Import cannot refresh the object because provider read access is denied.
Likely causeThe migration role can access state but lacks least-privilege read permissions for the remote object, uses a wrong provider alias, or encounters an organization policy boundary.
terraform providersVerify caller identity without printing credentials.Read provider audit-denial events.Query only the target object through the provider-native read API.
ResolutionGrant a bounded temporary read or import permission through the approved identity process, correct the provider alias, and retry the plan. Do not grant broad administrator rights or copy a static credential into provider configuration.
Removing an import block causes confusion in future reviews.
Likely causeThe team treated the block only as temporary code and lost the historical mapping from provider identity to Terraform address, or retained it without documenting its idempotent historical purpose.
git log -- imports.tfterraform state listReview the migration record and post-import plan.Confirm the imported object is still bound at the intended address.
ResolutionEither retain the applied import block as an explicit historical record or remove it in a separate documented change after state and configuration converge. In both cases preserve the address-to-ID manifest, approval, plan, and verification evidence.
Reference
Frequently asked questions
Does Terraform import change the remote object?
The import action binds an existing object to state, but the same plan can also contain configuration-driven changes. Accept only an import-only plan for the adoption window, then treat any desired remediation as a separate reviewed change.
Is generated configuration production-ready?
No. Official documentation still labels generation experimental and calls its HCL a best guess. Providers can produce conflicting, computed-only, deprecated, or overly literal arguments. A human must validate, simplify, and own it.
Can an import block target a resource inside a module?
Yes, current import blocks can address module and keyed instances, but the complete destination must exist under current module inputs. Review the exact address-to-ID map and require no create action.
Should applied import blocks be deleted?
Terraform allows retaining them as historical records or removing generated import blocks after adoption. Choose one documented convention; preserving the migration mapping and final empty plan matters more than the file location.
Recovery
Rollback
Before apply, rollback is deletion of unapplied import blocks and generated drafts. After apply, an import binding is state history: stop changes, preserve current and pre-import state, determine whether the object should remain managed, and use official state commands only under review. Never restore old state blindly or destroy the real object to undo an ownership mistake.
- Freeze the target root and every competing controller, preserve the current state object version, pre-import snapshot, source commit, plan, provider logs, and read-only remote-object inventory.
- If no import plan was applied, remove the import block and generated working file, rerun a normal plan, and require the pre-migration empty result.
- If a wrong address was imported but the object should remain managed, prefer a reviewed `moved` block or official `terraform state mv` procedure with backups, then plan every affected address.
- If Terraform should release the object without deleting it, use the current official removed/state workflow, verify the remote object remains healthy, remove its resource configuration, and document the new owner.
- If duplicate bindings exist, select one authoritative state, remove only the duplicate binding under dual control, and require an empty or fully explained plan in every root before resuming applies.
- Restore a historical state version only for confirmed state corruption after reconciling all remote changes since that serial; never use an old snapshot to erase a valid newer import blindly.
Evidence