Mission 1.4 · Stage 1 — State
Refactor Safely: moved, removed, and state mv
Goal: Rename the assets bucket resources to clearer names and hand a resource over to another team — with plans that show zero creates and zero destroys.
By the end of this mission
- Understand why renaming a resource in code means destroy-and-create
- Rename with
movedblocks so the plan shows no infrastructure changes - Stop managing a resource without deleting it using
removed - Know when the
terraform state mvCLI is still the right tool
Part 1
Understand it first
Addresses are identity
Terraform identifies resources by ADDRESS (aws_s3_bucket.assets), not by the real bucket. Rename the block to aws_s3_bucket.static_assets and Terraform sees two unrelated facts: an address in state with no config (destroy it) and an address in config with no state (create it). For a bucket with files or a database with data, that rename in a pull request is a production incident.
Renames happen all the time: better names, splitting a big file, adding count/for_each, moving code into modules (Stage 5). You need a way to tell Terraform 'same object, new address'.
moved blocks: renames as code
moved { from = OLD_ADDRESS to = NEW_ADDRESS } tells Terraform that the object recorded at the old address now lives at the new one. The plan shows has moved to and no infrastructure change. Because the block is in code, the refactor is reviewed in the same pull request, and it applies to EVERY state that uses this code: dev, staging, and prod each get moved on their next apply.
Leave moved blocks in place for a while after the refactor (or permanently in shared modules), so every environment and every consumer has a chance to apply them.
removed blocks: stop managing without destroying
Sometimes a resource should leave this configuration but keep existing: it's being handed to another team's config, or it will be managed by hand from now on. removed { from = ADDRESS lifecycle { destroy = false } } (Terraform 1.7+) removes it from state on apply and leaves the real object alone. Like moved, it's visible in the plan and reviewable. Without it, deleting the block would destroy the resource.
The CLI equivalents, terraform state mv and terraform state rm, change state immediately with no plan and no review. They're still useful for one-off repairs, but code blocks are the safer default.
Part 2
Your project after this mission · 4 files change
- infra/
- .terraform.lock.hcl
- backend.tf
- locals.tf
- logs.tf
- main.tfdeleted
- outputs.tfmodified
- providers.tf
- refactors.tfnew
- storage.tfnew
- terraform.tfvars
- variables.tf
- versions.tf
- .gitignore
Part 3
Build it, step by step
- 1
Why rename?
In Stage 4 ShopLite gets a second bucket for user uploads.
assetsis then ambiguous, so the team agrees onstatic_assets. You'll also move the bucket code out ofmain.tfintostorage.tf. The file move is free, because Terraform doesn't care about file names; the rename is the part that needs care. - 2
Rename the three resources in a new storage.tf
Create
storage.tfwith the renamed blocks and deletemain.tf. Note the references inside also change toaws_s3_bucket.static_assets.id.infra/storage.tfwhole filehcl resource "aws_s3_bucket" "static_assets" { bucket = local.assets_bucket_name tags = { Name = "${local.name_prefix}-assets" } } resource "aws_s3_bucket_versioning" "static_assets" { bucket = aws_s3_bucket.static_assets.id versioning_configuration { status = "Enabled" } } resource "aws_s3_bucket_public_access_block" "static_assets" { bucket = aws_s3_bucket.static_assets.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } - 3
Look at the plan WITHOUT moved blocks first
Update
outputs.tfto referenceaws_s3_bucket.static_assets, then plan. This is the plan a careless refactor would produce: three destroys and three creates, for a change that was only meant to rename things. Don't apply it.terminal$ sed -i 's/aws_s3_bucket.assets/aws_s3_bucket.static_assets/' outputs.tfterraform plan | tail -n 1── expected output ──Plan: 3 to add, 0 to change, 3 to destroy. - 4
Add moved blocks
One block per renamed resource. Keep them in a dedicated
refactors.tfwith a comment, so reviewers see the intent and the blocks are easy to delete later.infra/refactors.tfwhole filehcl # 2026-09 rename: "assets" → "static_assets" (uploads bucket arrives in Stage 4). # Safe to delete once every environment has applied it. moved { from = aws_s3_bucket.assets to = aws_s3_bucket.static_assets } moved { from = aws_s3_bucket_versioning.assets to = aws_s3_bucket_versioning.static_assets } moved { from = aws_s3_bucket_public_access_block.assets to = aws_s3_bucket_public_access_block.static_assets } - 5
Plan again: moves only
Now the plan lists each move and the summary is all zeros. Apply it; only state changes.
terminal$ terraform apply── expected output ──# aws_s3_bucket.assets has moved to aws_s3_bucket.static_assetsresource "aws_s3_bucket" "static_assets" {id = "shoplite-dev-assets-3f9a"# (11 unchanged attributes hidden)}# aws_s3_bucket_public_access_block.assets has moved to aws_s3_bucket_public_access_block.static_assets...# aws_s3_bucket_versioning.assets has moved to aws_s3_bucket_versioning.static_assets...Plan: 0 to add, 0 to change, 0 to destroy....Apply complete! Resources: 0 added, 0 changed, 0 destroyed. - 6
Hand a resource over with a removed block
Create a throwaway SSM parameter that, in our story, the data team will manage in their own repo from now on. Apply it, then replace the resource block with a
removedblock. The plan says it 'will no longer be managed by Terraform', and nothing is destroyed.infra/refactors.tfadd to filehcl First add and apply the resource block; then swap it for the removed block and apply again.
# Step A — apply this once: resource "aws_ssm_parameter" "reporting_endpoint" { name = "/shoplite/dev/reporting-endpoint" type = "String" value = "https://reports.internal.example.com" } # Step B — delete the block above and add: removed { from = aws_ssm_parameter.reporting_endpoint lifecycle { destroy = false } }terminal$ terraform apply── expected output ──# aws_ssm_parameter.reporting_endpoint will no longer be managed by Terraformresource "aws_ssm_parameter" "reporting_endpoint" {name = "/shoplite/dev/reporting-endpoint"...}Plan: 0 to add, 0 to change, 0 to destroy.Warning: Some objects will no longer be managed by TerraformIf you apply this plan, Terraform will discard its tracking information forthe following objects, but it will not delete them:- aws_ssm_parameter.reporting_endpoint...Apply complete! Resources: 0 added, 0 changed, 0 destroyed. - 7
Confirm the parameter still exists, then tidy up
AWS still has the parameter; Terraform no longer tracks it. Delete it by hand now, since it was only for the exercise, and remove the
removedblock too.terminal$ aws ssm get-parameter --name /shoplite/dev/reporting-endpoint --query Parameter.Value --output textaws ssm delete-parameter --name /shoplite/dev/reporting-endpoint── expected output ──https://reports.internal.example.com
Checkpoint — you should now have
- ✓
terraform state listshowsaws_s3_bucket.static_assetsand friends; nothing named.assetsremains. - ✓The rename applied with
0 to add, 0 to change, 0 to destroy. - ✓You used a
removedblock and confirmed the object survived in AWS. - ✓
refactors.tf(with the moved blocks) is committed, andmain.tfis gone.
Part 4
Break it on purpose
Make each change, run the command, and read the error before revealing the diagnosis. Recognising these messages on sight is what makes you fast on a real team. Undo the change afterwards.
Break #1
Keep the old block and add a moved block
Temporarily add back a resource block named aws_s3_bucket.assets (a copy of the static_assets one) while the moved block from assets still exists. Run terraform validate.
Break #2
Delete a moved block too early
Imagine a second environment (staging) that uses the same code but hasn't applied since the rename. Delete refactors.tf's moved blocks and plan that environment.
Part 5
Interview questions from this mission
You need to rename a resource that manages a production database. How do you do it without downtime?
Compare moved blocks with terraform state mv.
How do you stop managing a resource with Terraform without deleting it?