Command Palette

Search for a command to run...

Hectal

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.

30 min Free 7 steps 2 break-it drills

By the end of this mission

  • Understand why renaming a resource in code means destroy-and-create
  • Rename with moved blocks so the plan shows no infrastructure changes
  • Stop managing a resource without deleting it using removed
  • Know when the terraform state mv CLI 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.

Rename with and without moveddiagram
Rendering diagram…

Part 2

Your project after this mission · 4 files change

shoplite/
  • 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. 1

    Why rename?

    In Stage 4 ShopLite gets a second bucket for user uploads. assets is then ambiguous, so the team agrees on static_assets. You'll also move the bucket code out of main.tf into storage.tf. The file move is free, because Terraform doesn't care about file names; the rename is the part that needs care.

  2. 2

    Rename the three resources in a new storage.tf

    Create storage.tf with the renamed blocks and delete main.tf. Note the references inside also change to aws_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. 3

    Look at the plan WITHOUT moved blocks first

    Update outputs.tf to reference aws_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.tf
    terraform plan | tail -n 1
    ── expected output ──
    Plan: 3 to add, 0 to change, 3 to destroy.
  4. 4

    Add moved blocks

    One block per renamed resource. Keep them in a dedicated refactors.tf with 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. 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_assets
    resource "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. 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 removed block. 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 Terraform
    resource "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 Terraform
     
    If you apply this plan, Terraform will discard its tracking information for
    the following objects, but it will not delete them:
    - aws_ssm_parameter.reporting_endpoint
    ...
    Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
  7. 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 removed block too.

    terminal
    $ aws ssm get-parameter --name /shoplite/dev/reporting-endpoint --query Parameter.Value --output text
    aws ssm delete-parameter --name /shoplite/dev/reporting-endpoint
    ── expected output ──
    https://reports.internal.example.com

Checkpoint — you should now have

  • ✓terraform state list shows aws_s3_bucket.static_assets and friends; nothing named .assets remains.
  • ✓The rename applied with 0 to add, 0 to change, 0 to destroy.
  • ✓You used a removed block and confirmed the object survived in AWS.
  • ✓refactors.tf (with the moved blocks) is committed, and main.tf is 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.

terminal
$ terraform validate
── what you'll see ──
╷
│ Error: Moved object still exists
│
│ on refactors.tf line 4:
│ 4: moved {
│
│ This statement declares a move from aws_s3_bucket.assets, but that
│ resource is still declared at storage.tf:28,1.
│
│ Change your configuration so that this resource will be declared as
│ aws_s3_bucket.static_assets instead.
╵

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.

terminal
$ terraform plan # in an environment still at the old addresses
── what you'll see ──
# aws_s3_bucket.assets will be destroyed
# (because aws_s3_bucket.assets is not in configuration)
- resource "aws_s3_bucket" "assets" {
...
# aws_s3_bucket.static_assets will be created
+ resource "aws_s3_bucket" "static_assets" {
...
Plan: 3 to add, 0 to change, 3 to destroy.

Part 5

Interview questions from this mission

01

You need to rename a resource that manages a production database. How do you do it without downtime?

02

Compare moved blocks with terraform state mv.

03

How do you stop managing a resource with Terraform without deleting it?

0/4 · 0%