Why removed
Sometimes you need to stop managing a resource with Terraform without deleting it from the cloud. A few situations where this comes up:
- You are handing a system off to another team. They have their own
Terraform project and will capture the resource via
import. - You want to switch the resource to ClickOps or another tool, and the cloud resource must stay alive.
- You are splitting a monorepo into two states: half the resources
migrate. Use
removedin one state andimportin the other.
Before TF 1.7 you handled this with terraform state rm run by hand.
That leaves no trace in HCL, skips PR review, and is easy to forget
about. The removed block is declarative, the same way moved is.
Syntax
removed {from = aws_s3_bucket.legacy_logs
lifecycle {destroy = false
}
}
from: the resource (or module) address. Same format asmoved.from.lifecycle.destroy: what to do with the cloud resource.false: leave it in the cloud (the primary use case). The state releases the resource; it becomes unmanaged.true: delete it from the cloud (the same behavior as removing aresourceblock from HCL). This option is rarely needed. Usually it is simpler to just delete theresourceblock.
The resource block is removed from HCL at the same time. The removed
block is the instruction for how to remove it: with or without destroy.
Minimal example: giving up management
Before:
resource "aws_s3_bucket" "legacy_logs" {bucket = "linuxlab-old-logs"
}
You want to stop managing the bucket with Terraform, but the bucket must stay (it holds three years of data):
# deleted the resource block
removed {from = aws_s3_bucket.legacy_logs
lifecycle {destroy = false
}
}
terraform plan:
# aws_s3_bucket.legacy_logs will no longer be managed by Terraform,
# but will not be destroyed
bucket = "linuxlab-old-logs"
...
Plan: 0 to add, 0 to change, 0 to destroy.
After terraform apply, the state no longer contains the resource, but
the S3 bucket remains. Once apply completes you can delete the removed
block from HCL; it has done its job.
Use cases
Moving a resource between states
This is the most common scenario. State A drops the resource; state B
captures it via import.
Steps:
-
In state A (current):
hclremoved {from = aws_s3_bucket.data
lifecycle { destroy = false }}
Run
apply. State A no longer containsdata. -
In state B (new):
hclimport {to = aws_s3_bucket.data
id = "linuxlab-data"
}
resource "aws_s3_bucket" "data" {bucket = "linuxlab-data"
}
Run
apply. State B now owns the resource.
Between steps 1 and 2 the resource is unmanaged: nothing controls it.
That window is brief, but the order of steps matters. Always run
removed first, then import.
Removing an entire module
When you drop a whole module block:
removed {from = module.legacy_app
lifecycle {destroy = false # all resources inside the module stay in the cloud
}
}
One block releases all resources inside the module. Without it, Terraform would see a missing module and try to destroy everything that was in it.
With destroy = true
removed {from = aws_s3_bucket.temp
lifecycle {destroy = true
}
}
This is equivalent to deleting the resource block from HCL: the
resource will be destroyed in the cloud. The option exists for symmetry
with destroy = false. In practice, if you want a destroy, deleting the
resource block directly is simpler. removed with destroy = true
usually shows up in automated code generation or for declarative audit
trails.
removed vs state rm
| removed block | state rm |
|---|---|
| Lives in HCL, visible in diff | Only in shell history |
| Plan shows the change in advance | Writes to state immediately |
| Applies the same way for the whole team | One person ran it |
| TF 1.7+ only | Any version |
Requires removing the resource block at the same time | Does not touch HCL |
lifecycle.destroy = true gives a real cloud deletion | Removes from state only, does not delete in the cloud |
If TF 1.7+ is available, removed is always the better choice. The CLI
state rm command remains useful for emergency situations.
What to combine it with
importblock (tf-state-import): the typical step 2 after aremovedwithdestroy = false. Used for moving resources between states.movedblock (tf-moved-block): usemovedto relocate a resource rather than drop it. The two blocks sometimes appear together when a refactor both relocates some resources and releases others.- State backup before apply: required. Run
terraform state pull > backup.json. If something goes wrong,state push backup.jsonreturns the resource to management.
Pitfalls
-
lifecycle.destroy = falsedoes not mean "never destroy." It means only "do not destroy during the apply of thisremovedblock." If you later import the resource back into that state, normal management rules apply again, including a destroy when theresourceblock is deleted from HCL. -
Between the
removedapply in state A and theimportapply in state B, there is an unmanaged window. If someone makes changes via the console or another tool during that window, those changes will not be reflected in either state. Keep the window short. -
Deleting a
resourceblock without aremovedblock means destroy. TF 1.7+ surfaces this through theremovedblock pattern. If you delete a block in a hurry and run apply, the resource is gone. S3 versioning backups and planned PR reviews help prevent that. -
removed { from = module.X }does not distinguish between "drop the whole module" and "drop the module but keep the resources." The behavior is determined bydestroy = false|true. Read carefully when reviewing a PR that contains this. -
State lock applies. The
removedblock still writes to state, acquires a lock, and holds it until apply finishes. With a remote backend and DynamoDB that works normally. -
Not for address changes. To rename or relocate a resource, use
moved, notremovedfollowed by recreation.