Skip to content
SRE & DevOpsDeep Dive Published Updated 10 min readViews unavailable

Terraform removed Blocks: Decommission and Hand Off State Safely

Use Terraform removed blocks to destroy intentionally or detach infrastructure without destroying it, with reviewed plans, ownership transfer, and recovery controls.

Removing a resource block from Terraform configuration does not mean “stop managing this object.” If the object is still bound in state, Terraform normally plans to destroy it because the desired configuration no longer contains it. A removed block makes that transition explicit and reviewable: it can remove the binding and destroy the remote object, or remove the binding while leaving the infrastructure in place for another owner.

Use lifecycle { destroy = false } for a deliberate handoff or a controlled detach. Use destroy = true when the object itself should be deleted. Neither mode is a move to a new Terraform address in the same state; for a rename or module refactor, use a moved block. Choosing the wrong construct can either delete production infrastructure or leave it running without an owner.

The three different state transitions

Intent Preferred mechanism Infrastructure after apply
Rename or refactor an address in the same state moved block The existing object remains managed at its new address
Stop managing an object but keep it running removed block with destroy = false Object remains, but is no longer in that state
Stop managing and intentionally delete an object removed block with destroy = true Provider destroys the object and state forgets it

The removed block is configuration-driven and available in Terraform v1.7 and later. It allows the operation to appear in the normal plan and apply workflow instead of being an unreviewed manual state edit. For older versions, HashiCorp documents terraform state rm as an alternative, but it changes state directly and does not provide the same configuration-based preview.

Detach an object without deleting it

Replace the managed resource declaration with a removed block whose address matches the resource in the current state:

removed {
  from = aws_s3_bucket.legacy_logs

  lifecycle {
    destroy = false
  }
}

Terraform should plan to stop managing the resource while explicitly showing that the infrastructure will not be destroyed. Review that exact plan before applying. Remove configuration references that depend on the resource’s attributes; otherwise validation or planning can fail because the resource block no longer exists. Keep unrelated configuration changes out of the same operation so reviewers can isolate the ownership change.

The from address is important. Inspect the source state and use the exact module path and resource name. For a resource using count or for_each, a removed block cannot specify one instance key in from; the block targets the resource address rather than a selected indexed instance. If you need to retain some instances under Terraform management while detaching others, do not assume a single removed block can express that split. Design an explicit migration for the supported Terraform version and verify the exact resulting plan against representative state.

After apply, Terraform no longer refreshes or repairs the detached object. If the resource remains in configuration as a managed resource elsewhere, another state may plan to create a duplicate unless that state imports the existing object. A detach is not complete until the next owner, inventory system, or decommission record has accepted responsibility.

Destroy an object with an auditable plan

When the remote object should be destroyed, use a removed block with destroy = true:

removed {
  from = aws_s3_bucket.temporary_export

  lifecycle {
    destroy = true
  }
}

This makes the desired deletion explicit in configuration and plan output. Inspect the exact address, account, region, workspace, and provider before approving. For stateful infrastructure, confirm backup, retention, deletion-protection, dependent resources, and data-owner approval before applying. A well-formed plan proves what Terraform intends to request; it does not prove that the provider’s delete API will preserve data or satisfy an organization’s retention obligations.

Do not rely on prevent_destroy as protection after removing the resource block. That lifecycle setting only helps while its resource configuration remains present; removing configuration can still lead Terraform to destroy the object. A reviewed removed block communicates the intended destroy or detach transition directly and can be validated before apply.

Transfer ownership to another state

For a planned ownership transfer, coordinate the source and destination configurations as one change-controlled migration. Terraform 1.7 or later supports configuration-driven removal and import blocks, which can preserve a reviewable record:

Source configuration:

removed {
  from = aws_instance.worker

  lifecycle {
    destroy = false
  }
}

Destination configuration, where the resource block and provider configuration already exist:

import {
  to = aws_instance.worker
  id = "i-0123456789abcdef0"
}

The import ID is provider-specific and must identify the existing remote object exactly. Validate the destination configuration and provider’s import requirements before detaching the source. The source plan should show that the object will not be destroyed; the destination plan should show an import of that same object and no create or replacement. Apply the source and destination changes in a coordinated sequence with a clear owner and no competing Terraform runs. Then run fresh plans in both configurations: the source should no longer manage the object, and the destination should show it bound to the intended resource address.

There is a period during which one state may have removed its binding and the other has not yet imported it. Schedule the handoff, state locks, maintenance window, and rollback around that gap. Do not let both configurations independently manage the same object at once. Do not create a gap with no owner unless the object is intentionally being retired or the operational risk is explicitly accepted.

Removing a module is broader than removing one resource

A removed block can target a module address, such as module.legacy_network. With destroy = false, Terraform removes the module’s managed objects from that state without deleting the remote infrastructure. This is a broad ownership operation: inspect which resource instances live below the module address before planning a handoff, and verify that every object has an intended destination owner or retirement plan.

When using a module-level removed block, replace the module block rather than leaving both declarations in place. Review the plan for every object formerly managed by that module. A reusable module may also contain outputs or references used elsewhere; remove or replace those dependencies and validate the full configuration before apply. Do not assume a module-level detach transfers ownership to a new module automatically. If resources are moving to another address in the same state, prefer an address refactor with moved blocks where applicable.

A safe operational workflow

  1. Define the intent. Decide whether each object should be destroyed, retained but detached, or moved to another address. Document the future owner and recovery contact.
  2. Establish the exact state boundary. Record the Terraform version, backend, workspace, state lineage, provider alias, account, region, and resource address. Confirm that no other apply is running.
  3. Inspect before editing. Use read-only state inspection to identify all instances and relevant dependencies. Back up state using the backend’s supported procedure and verify state locking is enabled.
  4. Update configuration explicitly. Replace the resource or module declaration with a removed block for detach/delete, or use moved for a same-state refactor. Remove stale references and preserve required provider configuration until the plan is understood.
  5. Create and review a fresh plan. Confirm the expected destroy-or-detach action, all module children or instances affected, and the absence of unrelated changes. A plan from another workspace or stale revision is not approval for this one.
  6. Apply through the normal workflow. Maintain a single authorized apply owner. For a handoff, coordinate the destination import so there is no accidental duplicate resource or unmanaged gap.
  7. Verify both sides. Run a new plan and inspect state after apply. Confirm that the source no longer owns the object, the destination owns it when applicable, and the remote object has the expected state.
  8. Close the migration record. Keep the reviewed plan, owner, reason, object identifiers, backup reference, and rollback steps with the change record.

For a detach, rollback is not simply “revert the removed block.” If the resource configuration is restored without importing the existing object back into state, Terraform may plan to create a duplicate. Re-adopt the remote object using the provider’s import procedure, or coordinate the state recovery through the approved backend process. Never hand-edit state JSON as a shortcut.

Common failure patterns

Symptom Likely cause Safer response
Removing a resource unexpectedly plans deletion The resource block was removed without an explicit retained-object migration Stop before apply; use a removed block with destroy = false if the object must remain
Object remains in the cloud but the next plan proposes creation It was detached from one state without import into the new owner Import the existing object in the destination state before allowing normal applies
A removed block affects more instances than expected from identifies a resource address without an instance key Inventory state instances and redesign the change before applying
Module removal shows many object transitions from = module.name covers the module’s managed objects Review each child resource and decide the intended action individually
Plan fails with references to missing attributes Other configuration still uses the removed resource’s outputs Replace those expressions with new data sources, inputs, or destination outputs before planning
A state lock remains after a failed operation Another run may still be active or an earlier operation left a stale lock Follow the state-lock recovery runbook; force-unlock only after proving the lock is stale

The terraform state rm command is a direct state operation, not a substitute for reviewing a destroy plan. If a one-off manual removal is unavoidable, quote complex addresses carefully, verify the backend and workspace, capture a state backup, and ensure the object is not still declared in a configuration that will recreate it. Prefer a configuration-driven removed block when the required Terraform version is available.

Production acceptance checklist

  • The intent is unambiguous: same-state move, destroy, or detach for another owner.
  • The from address matches the current state, including module path; multi-instance behavior is understood.
  • Terraform version supports the removed workflow, and the backend’s normal lock and backup procedures are followed.
  • All attribute references, module outputs, provider configuration, and dependent resources are accounted for.
  • The plan shows the intended destroy or non-destroy action for every affected object and no unexplained changes.
  • A destination import is ready and tested before a cross-state handoff is applied.
  • Fresh post-apply plans prove state convergence and exactly one Terraform state owns each object.
  • Rollback re-import or backend recovery steps are documented rather than assumed.

removed blocks turn an ambiguous configuration deletion into an explicit state transition. Their production value comes from the plan: it tells reviewers which objects Terraform will destroy and which it will leave running but stop managing. Treat the destination owner, state boundary, and post-apply proof as part of the change, not as follow-up paperwork.

Related:

Sources:

Comments