Skip to main content

Refactoring and Import Clinic

Kata: Rename, relocate, and adopt resources without destroying them. This exercises Cluster 4 -- plan review, refactoring, and blast-radius control -- in the single most dangerous area of day-to-day Terraform.

You will do three things:

  1. Rename a resource inside the same module using a moved block.
  2. Extract a resource out of a root module into a child module using moved across the boundary.
  3. Adopt an existing, out-of-band resource with an import block.

Retrieval Prompts

  1. What happens if you rename a resource's address in .tf without a moved block?
  2. What is the difference between a moved block and terraform state mv?
  3. What does an import block do that terraform import (CLI) does not?
  4. What plan symbol should scare you the most, and why?
  5. What is prevent_destroy for, and what is its weakness?

Compare and Distinguish

  • Renaming a resource (moved) vs replacing a resource (new address, destroy + create). Which does your situation call for?
  • moved (refactor inside Terraform's knowledge) vs import (teach Terraform about something it does not yet own). Never confuse these.
  • terraform state rm (makes Terraform forget a resource exists) vs terraform destroy (deletes it). The words sound similar; the effects are opposite.
  • -target (narrow what you apply) vs a separate root module (narrow what Terraform sees). Why is the second almost always better?

The Clinic -- Three Drills

Drill 1: Rename with moved

  1. Start from Lab 1's state. Rename aws_s3_bucket.artifacts to aws_s3_bucket.artifact_store.

  2. Without a moved block, run terraform plan. Capture the output (destroy + create).

  3. Add the moved block:

    moved {
    from = aws_s3_bucket.artifacts
    to = aws_s3_bucket.artifact_store
    }
  4. Run plan again. Capture the output (no-op).

  5. apply. Run terraform state list and confirm the new address.

Drill 2: moved Across a Module Boundary

  1. Extract the bucket resource into modules/storage-bucket/.

  2. In the root, write:

    moved {
    from = aws_s3_bucket.artifact_store
    to = module.artifacts.aws_s3_bucket.this
    }
  3. plan -> should be a no-op against real infra.

  4. apply. Confirm the state shows module.artifacts.aws_s3_bucket.this.

Drill 3: Adopt with import

  1. Out of band (via the console or CLI), create a new S3 bucket named legacy-logs-<yourname>.

  2. In Terraform, add a resource block for it:

    resource "aws_s3_bucket" "legacy_logs" {
    bucket = "legacy-logs-<yourname>"
    }

    import {
    to = aws_s3_bucket.legacy_logs
    id = "legacy-logs-<yourname>"
    }
  3. Run plan. The plan should show import + possibly a ~ for attributes Terraform needs to set.

  4. Read the plan carefully. Fix the config until the plan is an import and nothing else.

  5. apply. Remove the import block after success.

Common Mistake Check

Identify and fix:

  1. moved { from = module.a.x to = module.b.x } where module a still exists and has its own x. What happens?
  2. An import block whose id is wrong (typo). Plan looks fine -- what does apply do?
  3. Renaming with terraform state mv in one engineer's terminal instead of using a moved block in code. What does the next PR reviewer see?
  4. An engineer saw replaced in a plan for production RDS and apply'd anyway because the PR description said "no-op." What is the process failure, not the engineer failure?
  5. prevent_destroy = true was added to a bucket, then an engineer removed it with a one-line PR to let destroy proceed. How do you prevent this pattern?

Evidence Check

This page is complete only when you can:

  • rename resources inside and across modules without a destroy/create
  • adopt out-of-band resources with import blocks and remove the block after success
  • read any plan and identify every ~, -/+, and <=
  • state what terraform state command you would use, and what command you would refuse to use, in an on-call incident
  • design a PR review policy for refactors that catches the mistakes above before apply