💻 Coding

Terraform Config Driven Refactor Planner: moved, import, and removed Blocks for Module Extraction, count to for_each Conversion, and Adopting Unmanaged Resources Without state mv

Plan a Terraform refactor that ends in a zero change plan: an address map from old to new resource addresses, moved blocks for module extraction and count to for_each keys, import blocks with generated config for resources created by hand, removed blocks with destroy set to false for resources you are handing off, the plan output you should expect, and a cleanup schedule for module consumers.

0.0
0Reviews
P
October 6, 2026

Prompt

Act as a Terraform platform engineer who refactors live state with config driven blocks instead of terraform state mv, so every change is reviewed in a pull request and proven by a plan that shows moves and imports with nothing to destroy.

Inputs:
- Terraform CLI version and provider versions from the lock file: [TerraformVersion]
- Current resource addresses from terraform state list: [StateList]
- The target layout (new modules, new resource names, new for_each keys): [TargetLayout]
- Resources that exist in the cloud account but not in state, with their real IDs: [UnmanagedResources]
- Resources to stop managing here without deleting them (handed to another team or stack): [HandOffResources]
- Who consumes these modules (only this root, or other teams pinning versions): [ModuleConsumers]
- Output format: [Format]

Generate:
1. A version check: moved blocks need 1.1 or later, import blocks 1.5 or later, removed blocks and for_each inside import 1.7 or later. If TerraformVersion is older, say which steps must wait or fall back to state commands.
2. An address map table: every StateList address, its new address from TargetLayout, and the block that handles it (moved, removed, or none).
3. moved blocks for module extraction (from = aws_x.name to = module.child.aws_x.name) and for count to for_each conversion with explicit index to key pairs. Never guess which index maps to which key; derive it from the attributes in StateList or ask.
4. import blocks for UnmanagedResources with to and id, plus the command terraform plan -generate-config-out=generated.tf and a note to review and trim the generated config before merging.
5. removed blocks for HandOffResources with lifecycle { destroy = false }, and a reminder to delete the matching resource blocks.
6. The expected plan summary: each move listed as has moved to, each import as will be imported, and 0 to add, 0 to change, 0 to destroy. List any line that would mean stop and fix before apply.
7. A cleanup schedule: when each block can be deleted, based on ModuleConsumers. Blocks in the root can go after a clean apply; moved blocks inside a shared module stay for at least one released module version when other teams pin it.

Constraints:
- Do not use moved to change a resource type unless the provider documents support for it.
- Do not invent resource IDs, ARNs, or attribute values. Mark unknowns as FILL FROM CONSOLE.
- Assume remote state with locking; never suggest editing the state file by hand. No em dashes.

Instructions

Replace every [bracket] with your details before running. Works on ChatGPT, Claude, and Gemini.

Generated Output

This image was generated using the prompt above.

Terraform Config Driven Refactor Planner: moved, import, and removed Blocks for Module Extraction, count to for_each Conversion, and Adopting Unmanaged Resources Without state mv - Result

Examples

Example Input

TerraformVersion: Terraform 1.8.5, hashicorp/aws 5.60
StateList: aws_s3_bucket.logs, aws_s3_bucket_versioning.logs, aws_iam_user.dev[0] (name=priya), aws_iam_user.dev[1] (name=marco), aws_security_group.legacy_bastion
TargetLayout: move both log bucket resources into module.logging with names this; convert aws_iam_user.dev to for_each keyed by user name
UnmanagedResources: an S3 bucket acme-ml-artifacts created by hand in the console
HandOffResources: aws_security_group.legacy_bastion now belongs to the network team's stack
ModuleConsumers: the logging module is also pinned by two other teams via a git tag
Format: address map, HCL blocks, expected plan, cleanup

Example Output

Version check
Terraform 1.8.5 supports moved, import, and removed blocks. No state commands needed.

Address map
| Old address | New address | Handled by |
| aws_s3_bucket.logs | module.logging.aws_s3_bucket.this | moved |
| aws_s3_bucket_versioning.logs | module.logging.aws_s3_bucket_versioning.this | moved |
| aws_iam_user.dev[0] (priya) | aws_iam_user.dev["priya"] | moved |
| aws_iam_user.dev[1] (marco) | aws_iam_user.dev["marco"] | moved |
| (not in state) acme-ml-artifacts | aws_s3_bucket.ml_artifacts | import |
| aws_security_group.legacy_bastion | none | removed |

refactor.tf
moved {
  from = aws_s3_bucket.logs
  to   = module.logging.aws_s3_bucket.this
}

moved {
  from = aws_s3_bucket_versioning.logs
  to   = module.logging.aws_s3_bucket_versioning.this
}

moved {
  from = aws_iam_user.dev[0]
  to   = aws_iam_user.dev["priya"]
}

moved {
  from = aws_iam_user.dev[1]
  to   = aws_iam_user.dev["marco"]
}

import {
  to = aws_s3_bucket.ml_artifacts
  id = "acme-ml-artifacts"
}

removed {
  from = aws_security_group.legacy_bastion
  lifecycle {
    destroy = false
  }
}

Then change the user resource to for_each = toset(["priya", "marco"]) with name = each.key, and delete the aws_security_group.legacy_bastion resource block.

Generate config for the import
terraform plan -generate-config-out=generated.tf
Review generated.tf: drop computed or default only arguments, move tags to your standard pattern, then rename the file to ml_artifacts.tf.

Expected plan
aws_s3_bucket.logs has moved to module.logging.aws_s3_bucket.this
aws_iam_user.dev[0] has moved to aws_iam_user.dev["priya"]
aws_s3_bucket.ml_artifacts will be imported
aws_security_group.legacy_bastion will no longer be managed by Terraform
Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
Stop and fix if you see: must be replaced, will be destroyed, or an in place update on the bucket. An update on the imported bucket usually means generated.tf lost an argument.

Cleanup
- The four moved blocks above live in this root and refer to this root's state, so delete them after this apply succeeds, along with the import and removed blocks.
- If you later rename resources inside the logging module itself, put those moved blocks inside the module and keep them for at least one tagged release, since two teams pin it by git tag.
- Confirm the network team has imported the security group before you delete the removed block.

Reviews (0)

Please login to leave a review.
Loading reviews...