Generate moved blocks and state move commands automatically for Terraform and OpenTofu.
Note
Status: stable. Used in production. Maintenance is mostly dependency updates: no new features are planned, but bug reports are welcome. Still v0.x because some internals may change in major ways before 1.0.
When you rename or move a resource in your Terraform code, Terraform loses track of the resource's state. The next plan shows the original resource being destroyed and a "new" one created in its place. tfautomv inspects the plan, detects these create/delete pairs, and writes moved blocks (or terraform state mv commands) so Terraform updates state in place without touching infrastructure.
For example, after renaming aws_instance.web to aws_instance.web_server:
resource "aws_instance" "web_server" {
ami = "ami-12345"
instance_type = "t2.micro"
}Running tfautomv produces a moves.tf file:
moved {
from = aws_instance.web
to = aws_instance.web_server
}The next terraform plan shows no changes.
On MacOS or Linux:
brew install busser/tap/tfautomvOn MacOS or Linux:
curl -sSfL https://raw.githubusercontent.com/busser/tfautomv/main/install.sh | shYay (Arch Linux), asdf, manual download, from source
Yay (Arch Linux):
yay tfautomv-binasdf version manager:
asdf plugin add tfautomv https://github.com/busser/asdf-tfautomv.gitManual download: grab a binary from the Releases page and put it in a directory on your PATH.
From source (requires Go 1.18+):
git clone https://github.com/busser/tfautomv
cd tfautomv
make buildThen move bin/tfautomv to a directory on your PATH.
Contributions to support other installation methods, including Windows for the shell script, are welcome.
Run tfautomv in any directory where you would run terraform plan:
tfautomvThis runs terraform init, terraform refresh, and terraform plan, then writes moved blocks to a moves.tf file. You can also target a specific working directory:
tfautomv ./productionBy default, tfautomv writes moved blocks. Force moved blocks only with --output=blocks:
tfautomv --output=blocksForce terraform state mv commands with --output=commands. The commands are printed to stdout, so you can review them, save them to a file, or pipe to a shell:
tfautomv --output=commands # print to stdout
tfautomv --output=commands > moves.sh # save to a file
tfautomv --output=commands | sh # run immediately-o is shorthand for --output.
If you have multiple Terraform modules in different directories, pass them all to tfautomv:
tfautomv ./production/main ./production/backup -o commandsThis runs terraform init, refresh, and plan in each directory, then writes terraform state mv commands to standard output. The commands move resources within and across directories as needed.
Terraform does not natively support moving resources across directories. To work around this, the generated commands pull copies of each directory's state, perform the moves locally, and push the new state back. You can pass as many directories as you want.
This requires the commands output format. Terraform's moved block syntax does not support cross-directory moves.
tfautomv runs init and refresh by default. To skip them and iterate faster:
tfautomv --skip-init --skip-refresh
# or, equivalently:
tfautomv -sStfautomv is for pure refactoring: restructuring code without changing infrastructure. Mixing refactoring with configuration changes (renaming a resource AND modifying its tags in the same step, for example) leads to bad matches or surprise infrastructure changes.
Recommended workflow:
- Make structural changes only (rename, move between modules, switch to
for_each). - Run
tfautomvand apply the resulting moves. Plan should show no infrastructure changes. - In a separate change, modify resource attributes as needed.
If a resource you expected to be matched is not, increase verbosity with -v (up to -vvv) to see why:
tfautomv -vvv| level 0 (default) | level 1 (-v) |
level 2 (-vv) |
level 3 (-vvv) |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
The output shows which attributes differ between create/delete pairs. Based on what you see, you can edit your code, write a moved block manually, or use --ignore (below) to skip specific differences.
tfautomv matches resources by comparing all their attributes. Sometimes a Terraform provider transforms an attribute's value (normalizing JSON whitespace, adding a prefix, etc.) so the value in your code never matches the value in state. The --ignore flag tells tfautomv to skip specific attributes during comparison.
Warning
Use --ignore for provider quirks, not for configuration changes you made on purpose. Forcing a match by ignoring an attribute you intended to change can produce unintended infrastructure changes when the move is applied. If you find yourself reaching for --ignore because you changed a value, that's a sign refactoring and configuration changes have been mixed: separate them (see Best practices).
A rule looks like this:
<KIND>:<RESOURCE TYPE>:<ATTRIBUTE NAME>[:<KIND ARGUMENTS>]
You can pass --ignore multiple times:
tfautomv \
--ignore="whitespace:azurerm_api_management_policy:xml_content" \
--ignore="prefix:google_storage_bucket_iam_member:bucket:b/"everything: ignore any difference. Example:--ignore="everything:random_pet:length"whitespace: ignore whitespace differences (useful for provider-formatted JSON or XML). Example:--ignore="whitespace:aws_iam_policy:policy"prefix: strip a fixed prefix before comparing. Example:--ignore="prefix:google_storage_bucket_iam_member:bucket:b/"
Detailed examples for each kind
whitespace allows these two resources to match despite different formatting:
resource "azurerm_api_management_policy" "foo" {
api_management_id = "..."
xml_content = <<-EOT
<policies>
<inbound>
<cross-domain />
<base />
<find-and-replace from="xyz" to="abc" />
</inbound>
</policies>
EOT
}
resource "azurerm_api_management_policy" "bar" {
api_management_id = "..."
xml_content = "<policies><inbound><cross-domain /><base /><find-and-replace from=\"xyz\" to=\"abc\" /></inbound></policies>"
}prefix with b/ strips that prefix before comparing the bucket attribute, useful when a provider stores b/my-bucket in state but the configuration sets my-bucket.
If you have a use case the existing kinds don't cover, please open an issue so we can track demand.
Join parent and child attributes with .:
<KIND>:<RESOURCE TYPE>:parent_obj.child_field
<KIND>:<RESOURCE TYPE>:parent_list.0
To find an attribute's full path, run tfautomv -vvv and read the verbosity output.
Use Terraform's built-in TF_CLI_ARGS and TF_CLI_ARGS_name environment variables. For example:
TF_CLI_ARGS_plan="-var-file=production.tfvars" tfautomvOpenTofu is supported out of the box via the --terraform-bin flag:
tfautomv --terraform-bin=tofuThis works with all features, including moved blocks, tofu state mv commands, and --preplanned.
Terragrunt does not work directly with --terraform-bin=terragrunt because Terragrunt's CLI does not behave identically to Terraform's. A wrapper script can bridge the gap. See issue #127 for the current discussion and an example wrapper.
The --terraform-bin flag works with any executable that exposes init and plan commands compatible with Terraform.
If you've already generated Terraform plan files, the --preplanned flag tells tfautomv to use them instead of running terraform plan. This is useful for:
- Performance: avoid re-running plans while iterating on
--ignorerules. - Enterprise environments: where running Terraform locally is impractical due to secrets or remote state.
- CI/CD workflows: where plans are generated in earlier pipeline stages.
- Remote workspaces (TFE/Cloud): where you can download JSON plans but can't run Terraform locally.
Basic usage:
terraform plan -out=tfplan.bin
tfautomv --preplannedCustom file paths, multiple directories, JSON vs binary plans
Custom plan file path:
terraform plan -out=my-plan.bin
tfautomv --preplanned --preplanned-file=my-plan.binMultiple directories (each must have its own plan file):
(cd production && terraform plan -out=tfplan.bin)
(cd staging && terraform plan -out=tfplan.bin)
tfautomv --preplanned production stagingJSON vs binary plans. tfautomv detects the format from the file extension:
- Binary plans (default): tfautomv runs
terraform show -jsonto convert them. - JSON plans (
.jsonextension): read directly.
terraform show -json tfplan.bin > tfplan.json
tfautomv --preplanned --preplanned-file=tfplan.jsonIf any specified directory is missing its plan file, tfautomv exits with an error.
Pass --no-color or set the NO_COLOR environment variable to any value:
tfautomv --no-color
NO_COLOR=true tfautomvtfautomv shells out to the Terraform (or OpenTofu) CLI, so it works with any compatible version. Specific features have minimum version requirements:
movedblocks: Terraform v1.1+- Cross-module
terraform state mvcommands: Terraform v0.14+ - Single-module
terraform state mvcommands: Terraform v0.13+
Thanks to Padok, where this project was born 💜
Apache 2.0. Summary.




