Moved and Removed Blocks
Scenario: A team splits one large, monolithic root configuration into several smaller, more maintainable modules - and the very next plan interprets every single resource that moved into a module as a brand-new object to create, with the old top-level ones marked for destruction.
You first met moved blocks in Module 5, for a simple rename within one root module; this section covers the fuller picture, because the exact same address-preservation problem shows up any time a resource's address changes for a structural reason - not just a rename, but exactly the kind of module-extraction refactor in the scenario above, or a change from count to for_each (Module 4). A moved block explicitly declares "the object that used to live at this old address now lives at this new address - they are the same object, preserve its identity," which is exactly what prevents the destroy-then-create Terraform would otherwise propose purely from the address changing.
removed blocks handle a related but distinct situation: telling Terraform to stop managing an object entirely, without necessarily wanting to destroy the real thing - useful when responsibility for an object is being handed off to a different tool or team. Depending on the lifecycle settings you attach and the Terraform version in use, a removed block can either destroy the real object as it's dropped from management, or simply forget about it while leaving the real object untouched (much like state rm from Module 5, but declared in version control rather than run as a one-off manual command).
Analogy: A moved block is like a mail-forwarding order filed with the post office after you change addresses. It has to stay active until every correspondent - every friend, every subscription, every bank - has actually learned and updated your new address in their own records. Cancel the forwarding order too early, and mail sent by anyone still using the old address to the correspondent's outdated file for you simply gets lost.
A worked configuration
moved {
from = fakecloud_server.api
to = module.service.fakecloud_server.api
}
removed {
from = fakecloud_server.retired
lifecycle { destroy = false }
}
Before rolling out a structural refactor like this across a whole team, plan the migration against a representative copy of real state first, roll it out in controlled stages rather than to everyone simultaneously, and make sure the old, pre-refactor version of the configuration genuinely cannot be applied by anyone after the migration lands - a straggler running old code against already-migrated state will find addresses it doesn't recognize. Treat a state-address-affecting refactor like this with the same seriousness as a database schema migration in application development - it is a deployment in its own right, with its own rollout plan, not just a code cleanup.
Common mistakes in production
- Extracting resources into a new module without adding
movedblocks for each relocated address - Terraform reads every one as delete-the-old, create-a-new, unrelated object. - Removing a
movedblock from the configuration too soon, before every collaborator's state has actually gone through the migration - anyone still behind gets stranded with unresolvable addresses.
Goal: Treat the plan as a change contract: understand every create, update, replacement, and destroy before approving it. Reinforce this lesson in the tf-moved-refactor Terraform lab. Open/labs/terraformand choose slugtf-moved-refactor; the lab runs real Terraform against the offline FakeCloud provider.