cswork / libraryOpen practice workspace

Articles

Terraform Refactoring Interview Questions: Import, Moved Blocks, and Safe State Changes

Terraform refactoring interview guide: moved blocks, imports, state mv, structured plan evidence, module moves, rollback, and safe ownership changes.

Terraform refactoring is safe when configuration addresses, remote objects, and state bindings remain intentionally aligned. A rename in HCL can look harmless yet produce a plan that destroys one address and creates another. A strong interview answer uses the plan as a proof obligation and chooses among a moved block, an import block, or an explicit state operation based on how object ownership is changing.

This guide focuses on existing infrastructure. For broader state and drift practice, read Terraform Interview Questions for Platform Engineers.

Evidence boundary: HashiCorp documentation is the official source for Terraform behavior. We found no reliable set of candidate reports supporting employer-specific claims, so these prompts are not attributed to any company.

Model three identities

Suppose state binds aws_instance.web to remote instance i-123. You want the configuration address to become module.compute.aws_instance.web.

Keep three identities separate:

  1. Configuration address: the resource address declared by HCL and module path.
  2. State binding: Terraform's record connecting an address to a remote object.
  3. Remote identity: the provider-side object ID and attributes.

Editing the configuration changes the first identity. It does not automatically move the second. Without refactoring metadata, Terraform can interpret the old address as removed and the new address as a new object.

The plan is the decision oracle. For a pure address refactor, the expected plan should say the object moved and show no remote create or destroy. If it says 1 to add, 1 to destroy, stop and explain the missing identity mapping.

Choose the operation by intent

SituationPrimary toolWhy
Rename a managed resource addressmoved blockpreserves the existing state binding declaratively
Move a managed resource into a modulemoved blockmaps old module path to new path
Adopt an existing unmanaged remote objectimport blockcreates a binding for an object Terraform did not manage
Repair an exceptional state bindingterraform state mv or other state commandimperative recovery with backup and review
Change provider resource typeprovider-specific migration or recreateschema and identity may not be compatible

Do not use import merely because the new address is absent from state. During a rename, the object is already managed at the old address. Importing it again can create duplicate ownership or confusing plans. Tell Terraform that the address moved.

Use moved blocks for durable refactor history

Starting with Terraform 1.1, a moved block can declare an old and new address:

moved {
  from = aws_instance.web
  to   = module.compute.aws_instance.web
}

HashiCorp documents that Terraform checks state for the old address and renames the object to the new address before planning. The resource is then treated as an existing object at the new address rather than destroyed and created.

A moved block belongs with the module or configuration history that needs it. Removing it later can be a breaking change for consumers upgrading from an older version that still has the old address in state. In a shared module, decide and document how long upgrade paths remain supported.

Address details matter. count and for_each instances have keys, so a move may need aws_instance.web[0] or aws_instance.web["blue"]. A resource-level move and an instance-level move express different mappings. Validate every affected instance in the plan.

Use import to adopt, not rename

Terraform import blocks describe an existing remote object and the configuration address that should manage it:

import {
  to = aws_s3_bucket.logs
  id = "company-production-logs"
}

The configuration must describe that object. After import planning, compare provider-read attributes with your intended HCL. Import does not prove the configuration is complete or policy-compliant; it establishes ownership so the plan can reveal differences.

Avoid importing one remote object to multiple addresses. HashiCorp assumes one-to-one ownership between a remote object and a Terraform resource address. Duplicate bindings can cause unpredictable management.

An interviewer may ask about generated configuration. It can accelerate adoption where supported, but it remains a draft. Remove computed-only noise, add required lifecycle and policy choices, and plan again.

Treat state mv as an imperative change

Before moved blocks, teams often ran:

terraform state mv \
  'aws_instance.web' \
  'module.compute.aws_instance.web'

The command changes the binding immediately. It can be appropriate for recovery or an older workflow, but coordination is harder: configuration and state must change in the right order, remote-state locking must work, and every environment must receive the same operation.

If using it, take a state backup through supported backend mechanisms, acquire the lock, confirm the exact source and destination with terraform state list and terraform state show, execute once, then plan immediately. Do not hand-edit state JSON. Do not disable locking to get past contention.

A remote backend may version state, but that is not permission to experiment in production. Test on a representative nonproduction workspace and rehearse restore. State can include sensitive values; protect backups accordingly.

Read the plan as structured evidence

A safe refactor plan has three layers:

  • Address evidence: old addresses map to intended new addresses exactly once.
  • Action evidence: no unapproved create, destroy, or replacement action appears.
  • Attribute evidence: provider values do not change merely because the address changed.

Use a saved plan and machine-readable output in CI when the change is sensitive:

terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json

Review resource_changes[].change.actions rather than trusting the human summary alone. Allowing only no-op and intended address moves is stronger than accepting “0 to destroy” while overlooking creates or in-place mutations.

The plan is still conditional on the refreshed remote state, provider version, credentials, workspace, and variables used. Record those inputs. A plan created against the wrong workspace can be internally consistent and operationally disastrous.

Handle module and for_each refactors

Moving one resource into a module is straightforward when the module contains the same resource type and provider configuration. Moving many count instances to for_each requires an explicit key mapping. Index 0 does not inherently mean key "primary"; that is a business decision.

For example:

moved {
  from = aws_instance.worker[0]
  to   = aws_instance.worker["primary"]
}

Create one mapping per instance when needed, then verify the remote IDs. If a key changes from an unstable list position to a stable name, prove that every old index maps to the correct semantic object.

Module splits also change outputs and caller contracts. A zero-resource-change plan does not prove downstream configurations still consume the right output names. Add module tests or staged consumer upgrades.

Know the stop conditions

Stop when the provider type changes and documentation does not guarantee a state migration. Stop when the current state already contains both old and new addresses. Stop when remote object IDs differ from your mapping. Stop when refresh reports unexpected drift. Stop when the backend lock cannot be acquired.

Do not use -target to make a confusing refactor plan appear small. Targeting is an exceptional recovery tool and can hide dependent changes. Do not apply with -refresh=false simply to suppress drift; investigate whether drift changes the safety of the move.

Provider aliases are another boundary. Moving a resource between provider configurations may change credentials, region, or account. An address move alone does not prove the remote object belongs under the new provider context.

A reviewable refactor runbook

  1. Pin and record Terraform and provider versions.
  2. Confirm backend, workspace, account, and region.
  3. List current addresses and remote IDs.
  4. Write the desired old-to-new mapping.
  5. Add configuration and moved or import blocks.
  6. Run format, validate, init, and a refreshed plan.
  7. Inspect structured actions and remote IDs.
  8. Apply in a lower environment or canary workspace.
  9. Re-plan and require no unexpected changes.
  10. Roll through remaining workspaces while monitoring provider-side effects.

Rollback must be designed before apply. Before a moved-block apply, reverting configuration and mapping may restore the old address if no incompatible attribute change occurred. After resource replacement or provider migration, rollback may require another state mapping or remote recovery. State restoration alone can lie about an object if the remote side changed.

Interview prompts and strong answers

“You renamed a resource and the plan wants replacement. Why?” Configuration has a new address, but state still binds the object to the old one. Add an explicit moved mapping and require a no-destroy plan.

“When do you use import?” When an existing remote object is not currently managed at any Terraform address and you want to adopt it. Import is not the normal solution for an address rename.

“Moved block or state mv?” Prefer a moved block for reviewable, repeatable configuration history and multi-workspace upgrades. Use state mv for controlled imperative recovery or legacy constraints, with lock, backup, and immediate plan verification.

“How do you prove safety?” Match old/new addresses and remote IDs, inspect structured plan actions, test representative workspaces, apply canary first, then run a post-apply no-drift plan.

“Can you delete moved blocks after apply?” Not automatically. Shared modules may have consumers upgrading from old state. Removing the block can break that upgrade path; treat it as a compatibility decision.

Account for multiple workspaces and partial rollout

A moved block can exist in one code version while different workspaces still run older versions. Build an inventory of workspace state serials and deployed module versions, then roll forward in a controlled order. If CI automatically applies every workspace, use a canary and pause gate instead of discovering an address mistake everywhere at once.

If one workspace fails after others succeed, keep the compatibility mapping and diagnose its state rather than deleting the moved block. Compare its old address, new address, provider identity, and remote ID with a successful workspace. The desired mapping may be universal while a legacy workspace contains exceptional drift that needs separate repair.

Common weak answers

“Terraform will detect the rename” ignores address identity. “Import everything again” risks duplicate ownership. “The summary says no destroy” may miss in-place mutations. “State is just JSON” encourages unsupported edits. “We can roll back the state file” ignores provider-side changes.

A concise answer is: “I classify the change as moving a managed address, adopting an unmanaged object, or changing remote identity. I encode moves declaratively, imports only for adoption, and use state commands only under a locked recovery runbook. The gate is a refreshed, structured plan with exact ID mapping and no unapproved remote action.”

Sources and Further Reading