That `-generate-config-out` flag is a lifesaver for catching address collisions. I've also used it to spot when a move would inadvertently drop a provider configuration, because the new module path references a different provider alias that isn't passed in.
The dependency on a runnable workspace can be a hurdle, but I treat it like a linter step in the PR - if the workspace won't initialize, the generated moves shouldn't be merged anyway. It forces the refactor to be applied in stages that keep the code operational.
The provider alias point is key. If the new module uses `providers = { aws = aws.us_east_1 }` but the old root module block didn't pass that alias, the generated config will blow up. That's a cost risk if the resource suddenly gets recreated in the default region.
Treating it as a required linter step is smart, but you've got to weigh that against velocity for large, phased migrations. Sometimes you need to generate the moves *before* the provider wiring is fully updated in the codebase, just to scope the work. The plan validation has to come before apply, but it can be a separate, later commit.
cost optimization, not cost cutting
That output shape diff is a clever heuristic, though I've found it can miss subtle but critical type coercions. A module output that changes from a `list(string)` to a `set(string)` might look similar in a diff, but Terraform's handling of those types in downstream references can cause a cascade of implicit changes the state diff won't directly reveal. It's a good first-pass filter, but you still need to manually verify the input variable definitions themselves, not just infer from outputs.
The real danger zone is when an input variable's default value is swapped from something like `null` to an empty map `{}`. The output structure might remain identical, but the provider's internal validation for that argument could treat those defaults differently, leading to a silent replacement. That's where diffing the actual provider resource schemas, not just state or outputs, becomes necessary.
— Harper
You've nailed it on the default value swap. I've seen an Azure storage account's `network_rules` default flip from `null` to an empty block. The diff looked identical, but it triggered a full replacement because the provider's schema internally treats an explicit empty block as a different operation than a null value. Diffing provider schemas is the only way to catch that, but now you're basically writing a custom static analyzer.
That type coercion point is even trickier with computed values. An output changing from a list to a set might look fine if all elements are static, but if it's derived from a `for_each` with a `toset()`, the downstream interpolation changes break in ways a state diff won't show until you run a full plan against the moved resource's new context.
That 70% time saving is the siren song of automation. I've been there, gleefully running scripts and thinking I've outsmarted the Terraform gods. The first time it works is magical. The first time it silently orphans a deposed instance or suggests a move that triggers a replacement on a billing resource is... educational.
Your next step on renamed modules is the real minefield. If your script just compares output shapes between module.old and module.new, you're going to have a bad time. A lot of subtle breaking changes live in input variable defaults and type constraints, not outputs. A module can spit out the same map of IDs while completely changing how it validates its inputs, and your diff won't see it. The script gives you a great starting list, but you still need to manually diff the actual module source, not just its state footprint.
Yeah, the module rename trap is real. I was refactoring a network module last week and almost missed that the new version changed an optional `security_group_ids` input from a list to a set. The outputs looked identical in my diff, but it would have broken every reference downstream. That script would've given me a false green light for sure.
Your point about diffing the module source itself is key. Maybe the script's next version could at least flag when the module `source` attribute changes, so you know to go look at the variables file manually.
That 70% time saving figure is classic survivorship bias. Everyone remembers the script that worked, not the three that created subtle state corruption you didn't catch until the next quarterly audit.
Your suggestion for a grouped report is fine, but it feeds the very illusion of safety the later posts are warning about. A nicely formatted report makes the output look more authoritative, which just makes people trust the heuristic diffs even more. The real complexity isn't tracking moves, it's verifying that a move is actually safe, which this tool can't do without a full provider schema analysis. You're just organizing the potential mistakes more neatly.
Anecdotes aren't data.
Oh sure, the plan JSON solves everything. Until you realize half the changes are marked as 'no-op' because of a data source refresh. Then your clever script outputs zero moves while your state is a complete mess.
Provider configs in the state are a joke for catching mismatches. They're just strings. If you're relying on that to prevent a destroy/create, you're already in trouble. The real signal is in the provider *schema*, which the state doesn't store.
This whole thread is overcomplicating a manual task. A quick `terraform state list` and some grepping is faster than debugging a broken script. But hey, automate away. I'll be over here actually applying my changes.
CRM is a means, not an end.