# tfautomv: writing Terraform moved blocks by reading the plan

> tfautomv runs a Terraform plan, finds the destroy and create pairs that a rename produced, and writes the moved blocks or state mv commands that repair the state without touching infrastructure. It is a narrow tool with a stable status, a documented rule against mixing refactoring with configuration changes, and no cross-directory support in its moved block output.

**busser/tfautomv** — Generate Terraform moved blocks automatically for painless refactoring

- Repository: https://github.com/busser/tfautomv
- Website: https://github.com/busser/tfautomv
- Stars: 898 · Forks: 26
- Language: Go
- License: Apache-2.0
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/busser-tfautomv

## A rename in Terraform looks like a destroy and a create

The problem tfautomv solves is a specific one. 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, even though you only changed a label. The tool inspects the plan, detects these create and delete pairs, and writes moved blocks, or terraform state mv commands, so Terraform updates state in place without touching infrastructure. The worked example in the README is a rename of aws_instance.web to aws_instance.web_server, and the resource after the rename looks ordinary:

```terraform
resource "aws_instance" "web_server" {
  ami           = "ami-12345"
  instance_type = "t2.micro"
}
```

What you get back is a file of moves, and the next terraform plan shows no changes at all. The value is that the risky part of the operation, touching real infrastructure, is avoided entirely, and the tool is doing the pairing that a human would otherwise do by reading plan output side by side.

## Running it is one command in a directory you would already plan in

The tool is installed in the usual ways. On MacOS or Linux, Homebrew and a shell script are both offered:

```bash
brew install busser/tap/tfautomv
```

```bash
curl -sSfL https://raw.githubusercontent.com/busser/tfautomv/main/install.sh | sh
```

The details block adds Yay for Arch Linux, the asdf version manager, a manual download from the Releases page, and a from source route that needs Go 1.18 or newer and a make build, after which you put bin/tfautomv on your PATH. The usage is then a single command in any directory where you would run terraform plan:

```bash
tfautomv
```

Under the hood it runs terraform init, terraform refresh and terraform plan, and then writes the moved blocks to a moves.tf file. You can point it at a specific working directory with tfautomv ./production. The one thing to be aware of is that it is a Go program that shells out to the real Terraform binary, and the go.mod shows that it uses HashiCorp's terraform-exec and terraform-json libraries, so the tool and your Terraform version have to agree about the plan format.

## Blocks or commands, and the difference matters

By default the tool writes moved blocks, which is the form that lives in your configuration and gets applied on the next plan. The output format can be forced either way, and -o is the shorthand for --output. The commands form is printed to stdout so you can review it, save it, or pipe it to a shell:

```bash
tfautomv --output=commands              # print to stdout
tfautomv --output=commands > moves.sh   # save to a file
tfautomv --output=commands | sh         # run immediately
```

The distinction is a workflow decision, not a cosmetic one. Moved blocks are declarative, live in version control, and are re-applied every plan, which is the safer default for a team that wants the rename to be permanent and reviewable. State mv commands are imperative, they run once, and they change the state file directly, which is what you want when the move involves directories. The README does not frame this as a recommendation, but the fact that cross-directory moves require the commands format makes the block default a sensible choice for most work.

## Cross-directory moves need the command output, and the mechanism is a copy

Terraform does not natively support moving resources across directories, and tfautomv works around that in a way worth understanding before you rely on it. When you pass several module directories, the tool runs terraform init, refresh and plan in each one, then writes terraform state mv commands to standard output. The commands move resources within and across directories as needed. The workaround itself is that the generated commands pull copies of each directory's state, perform the moves locally, and push the new state back, and you can pass as many directories as you want. That means the state files are fetched, edited and uploaded, which is a heavier operation than a local state edit and deserves a careful eye in a pipeline. This path requires the commands output format, because Terraform's moved block syntax does not support cross-directory moves at all. That is a hard boundary, and it means a rename that spans directories and a rename that stays inside a module are two different workflows with two different outputs.

## The rule that keeps the tool honest: refactoring only

The best practices section is short and it is the part to read first. tfautomv is for pure refactoring, which the README defines as restructuring code without changing infrastructure, and mixing refactoring with configuration changes, renaming a resource and modifying its tags in the same step for instance, leads to bad matches or surprise infrastructure changes. The recommended workflow has three steps. Make structural changes only, such as a rename, a move between modules, or a switch to for_each. Then run tfautomv and apply the resulting moves, and the plan should show no infrastructure changes. Then, in a separate change, modify resource attributes as needed. Separating the two is what makes the matching reliable, because the matcher compares every attribute of the create and delete pairs, and an intentional attribute change is indistinguishable from a mismatch at that level. The tool is a good fit for the first kind of change and a poor fit for the second.

## Unmatched pairs, verbosity, and the ignore flag you should use carefully

When a resource you expected to be matched is not, the tool can tell you why. Increasing verbosity with -v, up to -vvv, shows which attributes differ between create and delete pairs, and the repository holds one screenshot per verbosity level so you can see what each looks like. The output shows which attributes differ, and from there you can edit your code, write a moved block by hand, or use --ignore to skip specific differences. The ignore mechanism exists because providers transform attribute values, normalising JSON whitespace or adding a prefix, so the value in your code never matches the value in state. The rule for ignoring a specific attribute is a four part pattern:

```plaintext
<KIND>:<RESOURCE TYPE>:<ATTRIBUTE NAME>[:<KIND ARGUMENTS>]
```

The warning attached to it is the important part. Use --ignore for provider quirks, not for configuration changes you made on purpose, because forcing a match by ignoring an attribute you intended to change can produce unintended infrastructure changes when the move is applied. The advice that follows is the most useful sentence in the documentation: if you are reaching for --ignore because you changed a value, that is a sign that refactoring and configuration changes have been mixed, and the fix is to separate them.

## Skipping init and refresh makes iteration fast, and the plan is stale

Running terraform init and terraform refresh on every invocation is slow, and the tool gives you a way out. Both steps run by default, and both can be skipped, with a short form for the pair:

```bash
tfautomv --skip-init --skip-refresh
# or, equivalently:
tfautomv -sS
```

The trade is straightforward and the tool is honest about it. Skipping refresh means the state the matcher compares against is not up to date, so a resource changed outside Terraform since the last run will look different from the code and will fail to match, or will match on an attribute you did not intend to compare. Skipping init means the provider plugins and modules are not re-fetched, so a configuration that just added a new provider will fail in a way that looks like a tool bug. The sensible use is the second iteration on the same change, where the state is known to be fresh because you have just run it once, and the wrong use is the first run after someone else changed the infrastructure. The alternative to all of this is the hand-written moved block, which is more typing and no risk of a stale comparison.

## Stable status, no planned features, and a Go project with release tooling

The project states its own position in a note at the top of the README, and it is unusually direct. The status is stable, it is used in production, and maintenance is mostly dependency updates. No new features are planned, but bug reports are welcome, and it is still on a 0.x version because some internals may change in major ways before 1.0. The licence field on the repository says Apache-2.0, and the build tooling is a Go project with a Makefile whose release target tags the version, pushes the tag and hands the release notes to goreleaser, with a dry-run target that exercises the same path without publishing. The test targets cover both unit tests over the pkg directory with coverage and end to end tests in a separate directory, which is the right split for a tool whose output is compared against real Terraform behaviour. The release history shows v0.7.0 in June 2025 and two 0.6.x releases in February 2024, and the last push was on 2026-10-20. Renovate is configured for dependency updates, and a VERSION file holds the version the Makefile reads, which is why the release process can produce a tag without a version bump in the source.

## Conclusion

tfautomv suits a team that renames or restructures Terraform resources often enough that hand-writing moved blocks has become a chore, and that can follow the documented rule of separating structural change from attribute change. It does not suit a team whose providers normalise attributes in ways the matcher cannot see, since every unmatched pair has to be forced or written by hand. Before adopting it, run it with -vvv on one rename and read which attributes differ, decide whether the output goes into version control as moved blocks or into a reviewed script, and remember that the tool is declared stable with no new features planned, so the version you adopt is the version you will keep.

## FAQ

### How do I install tfautomv?

On MacOS or Linux, install it with brew install busser/tap/tfautomv or with the install.sh script piped to sh. The project also documents a Yay package for Arch Linux, an asdf plugin, manual download from the Releases page, and building from source with Go 1.18 or newer, which needs a make build and putting bin/tfautomv on your PATH.

### What does tfautomv actually do when I run it?

It runs terraform init, terraform refresh and terraform plan, then detects the create and delete pairs that a rename produced and writes moved blocks to a moves.tf file, or prints terraform state mv commands if you ask for the commands format. The next plan then shows no changes.

### When should I use --output=commands instead of moved blocks?

The commands format is required for moving resources across directories, because Terraform's moved block syntax does not support cross-directory moves. The generated commands pull copies of each directory's state, perform the moves locally and push the new state back, so the mechanism is heavier than a local state edit.

### When is it wrong to use tfautomv's --ignore flag?

It is meant for provider quirks, such as a provider that normalises JSON whitespace or adds a prefix, so the value in your code never matches the value in state. The documentation warns against using it for configuration changes you made on purpose, since forcing a match that way can cause unintended infrastructure changes when the move is applied.

## Sources

- [busser/tfautomv on GitHub](https://github.com/busser/tfautomv)
- [License: Apache-2.0](https://github.com/busser/tfautomv/blob/main/LICENSE)
- [Project website](https://github.com/busser/tfautomv)
- [README](https://github.com/busser/tfautomv/blob/main/README.md)
- [Releases](https://github.com/busser/tfautomv/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/busser-tfautomv
