Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Terraform By James Joyner IV · · 9 min read

Retiring Resources Safely With the Terraform removed Block

Use the Terraform removed block (1.7+) to declaratively drop resources from state without destroying real infrastructure. The modern replacement for state rm.

  • #terraform
  • #state
  • #refactoring
  • #removed
  • #ai

The first time I ran terraform state rm against a production workspace, my hands were a little sweaty. I was decommissioning a legacy EC2 box that another team had taken ownership of, and I needed Terraform to stop managing it without tearing it down. The command worked, but it was an imperative, out-of-band side effect — nothing in my configuration recorded why the resource left state, and a teammate reviewing the next PR had no way to see what I’d done. Since Terraform 1.7, there’s a far better answer: the removed block. It turns “drop this from state” into a reviewable, declarative change that lives in your code and shows up in terraform plan.

The problem with terraform state rm

The classic way to stop managing a resource without destroying it was a manual CLI command:

terraform state rm aws_instance.legacy

This works, but it has real drawbacks. It’s imperative — it mutates state immediately, outside of any plan/apply cycle. It leaves no trace in your configuration, so code review can’t catch it. And it’s easy to fat-finger the wrong address. Anyone auditing the repo months later sees a resource that simply vanished from state with no explanation. For a one-off in a scratch workspace that’s fine. For shared infrastructure, it’s the kind of undocumented action that causes 2 a.m. confusion.

Enter the removed block

The removed block (Terraform 1.7+) lets you express the same intent in configuration. You delete the resource block as usual, then add a removed block pointing at the address you’re retiring:

# The aws_instance.legacy resource block has been deleted.

removed {
  from = aws_instance.legacy

  lifecycle {
    destroy = false
  }
}

The lifecycle { destroy = false } is the key. It tells Terraform: forget this resource exists in state, but leave the real infrastructure running. When you run a plan, Terraform reports the resource will simply leave management rather than being destroyed:

Terraform will perform the following actions:

  # aws_instance.legacy will no longer be managed by Terraform, but will not be destroyed
  # (destroy = false is set in the relevant removed block)
 . resource "aws_instance" "legacy" {
        id = "i-0a1b2c3d4e5f67890"
        # (resource arguments)
    }

Plan: 0 to add, 0 to change, 0 to destroy.

Notice 0 to destroy. The EC2 instance keeps running; Terraform just stops tracking it. After you apply, you delete the removed block too — it’s a transitional construct, not something you keep around forever.

Pro Tip: The lifecycle block inside removed only accepts destroy. There is no count or for_each there — those go on the resource address itself (more on that below). If you forget the lifecycle block entirely, destroy defaults to true, which is almost certainly not what you want when decommissioning a still-needed resource.

When you do want it gone: destroy = true

Sometimes the goal really is to destroy the resource, and the removed block can do that too. This is useful when you’ve already deleted the resource block from configuration and want an explicit, reviewable record that the deletion is intentional:

removed {
  from = aws_s3_bucket.scratch

  lifecycle {
    destroy = true
  }
}

With destroy = true, the plan shows a normal destroy operation. The advantage over just deleting the resource block silently is auditability: the removed block documents the decision in the diff, and reviewers see it explicitly rather than inferring it from an absence. Either way, choose destroy deliberately — it’s the single most important field in the block.

Moving resources out of a module

A common refactor is pulling a resource out of a module so it lives at the root, or retiring a module wholesale. The removed block handles module addresses cleanly. Suppose you had module.networking.aws_eip.nat and you want to stop managing it without releasing the Elastic IP:

removed {
  from = module.networking.aws_eip.nat

  lifecycle {
    destroy = false
  }
}

If you’re actually relocating a resource (keeping it managed, just at a new address), that’s a job for moved, not removedmoved preserves management, removed ends it. Reach for removed only when Terraform should let go of the resource entirely. When you’re decommissioning an entire module, you can also target the module address directly:

removed {
  from = module.legacy_vpc

  lifecycle {
    destroy = false
  }
}

Working with for_each and counted resources

Resources created with for_each or count use indexed addresses, and removed supports them. To remove every instance of a for_each resource, point from at the resource address without an index:

removed {
  from = aws_iam_user.contractors

  lifecycle {
    destroy = false
  }
}

That removes the whole collection from state. The plan output lists each instance individually, each one tagged “will no longer be managed”:

  # aws_iam_user.contractors["alice"] will no longer be managed by Terraform, but will not be destroyed
  # aws_iam_user.contractors["bob"] will no longer be managed by Terraform, but will not be destroyed

Plan: 0 to add, 0 to change, 0 to destroy.

If you only want to remove specific instances, the cleaner pattern is to adjust the for_each map so those keys no longer exist, and let Terraform handle it — but for a full retirement, the collection-level removed block is exactly right.

Letting AI draft the blocks (and why a human still owns the plan)

Writing removed blocks for a sprawling refactor — a dozen resources moving out of three modules — is tedious and error-prone. This is the kind of mechanical, well-specified work where an AI assistant shines. I treat the model like a fast, eager junior engineer: I paste the current module structure and the list of addresses I’m retiring, and ask it to generate the removed blocks with the correct from addresses and the right destroy value for each. Tools like Claude or Cursor turn out a first draft in seconds, and our curated prompts help frame the ask so the output is consistent.

But the draft is exactly that — a draft. The non-negotiable rule on my team: AI never auto-applies, and a human reviews every single plan. The removed block is one keyword away from disaster — flip destroy = false to destroy = true and you’ve torn down production instead of releasing it from state. So the workflow is strict:

  • The model only ever sees configuration and gets to propose HCL.
  • The model never receives state-write access, and never gets cloud credentials.
  • A human runs terraform plan and reads it line by line, confirming 0 to destroy (or an intentional destroy count) before anyone applies.

That plan output is the safety gate. If the AI got an address wrong, the plan surfaces it as an unexpected change. If it set the wrong destroy value, the destroy count gives it away. For higher-stakes refactors I route the diff through our code review dashboard so a second set of eyes — human or assisted — checks it before merge.

Pro Tip: Pin your config to the version that supports this feature so a teammate on an older CLI gets a clear error instead of a confusing parse failure: terraform { required_version = ">= 1.7.0" }. The removed block simply doesn’t exist before 1.7.

Wrapping up

The removed block transforms a nervy, undocumented terraform state rm into a reviewable change that lives in your code and shows its intent in the plan. Use destroy = false to retire resources from management while leaving infrastructure intact, destroy = true for deliberate teardown, and lean on it for module extractions and for_each cleanups. Let AI draft the blocks to save time — then own the plan yourself, because that human review is what keeps “stop managing this” from quietly becoming “destroy this.” For more on safe Terraform refactoring, browse the rest of the Terraform category.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.