Request for Feedback: Cross-package move statements

In terraform 1.1.0 , it was announced that there was a new moved{} block to enable refactoring.

However, it seems that if you want to refactor into a module managed outside of the current repo, this is not allowed - and not mentioned anywhere.

Error: Cross-package move statement

  on .terraform/modules/example/main.tf line 4:
   4: moved {

This statement declares a move to an object declared in external module
package "git::ssh://git@github.com/user/terraform-module.git". Move
statements can be only within a single module package.

However, this seems like a very artificial limitation. terraform state rm/terraform import works just fine going to external modules. External modules may very well be managed by the same team, just in separate repos. Plan files also don’t seem to have any real difference between modules in the same repo and in separate ones.

Only being able to have auditable refactoring within the same repo is quite useless to the challenges I’m facing here. Is there a way to say “let me move to external modules anyways”, or a timeline for this limitation to be removed?

4 Likes

Hi @rgooler,

The following paragraphs in the documentation above moving between modules discuss this current restriction:

The multi-module refactoring situation is unusual in that it violates the typical rule that a parent module sees its child module as a “closed box”, unaware of exactly which resources are declared inside it. This compromise assumes that all three of these modules are maintained by the same people and distributed together in a single module package.

To reduce coupling between separately-packaged modules, Terraform only allows declarations of moves between modules in the same package. In other words, Terraform would not have allowed moving into module.x above if the source address of that call had not been a local path.

In other words, declaring that something moved into a child module declared elsewhere would impose a compatibility constraint on that other module without it being recorded in that module, and thus maintainers of that module would need to preserve compatibility without necessarily knowing that there’s something to preserve compatibility with.

If you are in control of both modules and in control of making sure that the caller switches to the new version of the called module at the right time for the move to make sense then you can use terraform state mv to tell Terraform to complete the move at that point, just as before.

The requirement for this to be allowed would be to have some new syntax for an external module to declare that it will accept a move from outside to a particular address, so that there is some record in the called module that there is an external interface that other modules may be relying on. That extra feature wasn’t part of the scope of the first phase of this feature, but it’s a likely future enhancement.

However, in this desire to reduce coupling, several tradeoffs are made without the user’s ability to override them.

  • terraform state mv is a highly-privileged operation which can cause friction in any environment where PRs are accepted and automated - but direct state manipulation is restricted.
  • Users may wish to transition responsibilities into another repository - owned locally or externally - and then clean up afterwards
  • terraform state mv doesn’t leave an audit log of infrastructure changes in the code repository
  • These artificial barriers vanish for anyone using a monorepo and git submodules for third party modules. If this coupling issue is more important than users making their own decisions over their own environments, then why is this restriction only applicable under some repo configurations?

Additionally, the proposed solution of creating a bidirectional approval process means that I, the user, am not permitted to migrate my dodgy code into a community supported module UNLESS I’m willing and able to modify state files directly, which is an action that is very difficult to peer review. This seems antithetical to the concept of refactoring in the first place.

If you want to make this a guardrail to prevent certain decisions from being made, then I believe that a few changes should be made:

  1. Improve the error message to link to documentation describing the coupling issue. The multi-module refactoring situation (especially as it relates to externally hosted modules) is not clearly laid out.
  2. Allow a force = True option. Operationally, even if I shouldn’t do this, sometimes I have to do bad practice to keep the site online. Additionally, for anyone using Terraform along with a tool such as OPA - this creates an easy point to say “Hey, force = True in a moved{} block requires approval”.
  3. Consider, when enhancing this and any other refactoring features - that part of the goal for users is to have tools that help them get out of a bad place. Having to bail out to running the command line tools breaks the concept of “infrastructure as code”, as its all code… except for this bit which was someone applying duct tape and hoping they didn’t make a mistake.

I think that moved is an amazing feature to have, but unfortunately these limits reduce it from being an extremely powerful tool to a tool that is all but unusable for the needs of myself and the users I support.

3 Likes

Thanks for the feedback, @rgooler!

The first iteration of this set of features is intended for a more “intentional” sort of refactoring where a single person is changing both the source and the destination in a single VCS commit as part of a carefully-planned refactoring, rather than for the slightly different scenario you seem to be targeting of digging out of a “messy” situation.

No new feature can possibly meet all use-cases in its first iteration, or it would basically never ship, but we do hope to learn from experiences and feedback like this about the initial feature and cautiously expand the scope of this feature over time. As we do so, we need to make sure that we don’t inadvertently create new burdens for the maintainers of shared modules (by turning what was previously implementation detail into a public interface they must be compatible with, without their consent) and make sure that the new features are hard to use incorrectly in a way that could cause inadvertent distruption to infrastructure.

That does mean that for now not all use-cases can be met by this feature, and so those use-cases will need to continue to be handled as before using terraform state mv. That doesn’t mean that’ll be the case forever, but it is the case for the first iteration in the v1.1 series.

I would also love this to work when using “remote” module sources. We manage our entire ecosystem of modules, so we have complete control over how they are used and would like to be able to have a “force” option or for it to just be a first class citizen.

1 Like

I never had issues with it in 1.1.2 until I hit it now in 1.1.3. After upgrading to 1.1.4 the error is gone.

The behaviour is a bit unexpected, because this:

moved {
   from = module.request_response_topic.kafka_acl.this["foo"]
   to   = module.request_response_topic.kafka_acl.this["bar"]
}

fails with 1.1.5, throwing:

This statement declares a move to an object declared in external module package

The module we use for this “kafka_topic” is under our control, and we have changed the way the resources are indexed in a for_each loop. The nature of the keys is dynamic, so we cannot add moved {} blocks in the module, but could in the project that uses the module. The modules are fetched over git+https, which makes them external (?), and here, we are with no way to migrate.

Out project teams don’t have permissions to mangle in the statefiles for security reasons, so there seems to be no way to migrate the state without calling admins or destroying and recreating resources. The latter is even more problematic, because terraform will sometimes first create the new resource, and then delete it, because of monkey/kafka provider works - that’s a different topic though.

1 Like

@mkielar @grimm26 @rgooler

Hi all! I’m the Product Manager for Terraform Core, and I’m currently writing out the requirements for this request. If anyone would be open to chatting with me about this, I’d love to speak with you. You can book time on my calendar here, and if you’d rather chat over email, you can shoot me an email at kalpers@hashicorp.com. Otherwise, I’ll review all the comments here. Thanks again for the feedback on this issue, and excited to improve it.

1 Like

I have a use case where i have a local module where i use some public ones.
This restriction does not allow me to rename a module, even if it is not a very complex refactor but i still am not sure why am i encouraged to use terraform state mv instead of using this moved feature?

moved {
  from = module.name1.module.security_group.this
  to   = module.name2.module.security_group.this
}

Respectfully: “the statefile should be a black box and nobody should ever have to touch it” is an admirable end state to push for. Maybe the product will get there in another 3 years. But today, this is wholly out of line with the realities that professionals working with Terraform face on a daily basis. When these artificial gates are imposed on users, they cost companies using Terraform literal person-years in engineering time annually while we build complex orchestration around changes to prevent Terraform from arbitrarily destroying critical infrastructure.

We buy products like Terraform Enterprise on the promise that the approach keeps us from having to write bespoke state transitions for all of our infrastructure. But instead, we get stuck writing bespoke state transitions between Terraform module versions. We try to help users make safe changes by writing convenient wrapper modules, and then we’re artificially gated out from using the features that were supposed to make these patterns ergonomic and safe and possible for our end-users. We ask for features to make our lives easier, but they get slimmed down and rescoped until they’re only helpful to vendors in HashiCorp’s partner ecosystem, not the people using Terraform to make something happen in their business.

Platform engineers’ lives are hard, and all we want is to provide a platform to our users that they can trust. What we keep finding is that every time they touch something, they have to come to us to make sure Terraform isn’t about to delete some critical part of their infrastructure. And half the time, it is. The wheels have fallen off the cart, here. Please stop dangling the wrench on a fishing line just out of reach.

1 Like

Here I am adding another use case where the limitation should NOT be imposed:

Our setup invovles different environments (dev, stg, qa, uat, prod, etc) managed in different directories. These environments share the same module. It might take a few months for a change to propagate from dev to prod, so we need to use versioned modules. Because a module sourced from local filesystem does not support versioning, we publish the modules to our Terraform Enterprise private registry.

Typically a new feature goes through this process before going live in production:

  1. the code is added to the module
  2. a new version of the module is published in TFE private registry
  3. the new version is tested in dev
  4. the new version is propgated to other environments
  5. the new version finally reached prod

Sometimes, we may add a new functionality to dev directly for quick PoC. When the new feature is proved to work well, we refactor the code into the private module and publish it on TFE so that other environments can use the updated module.

We are excited about the “moved” syntax. But with this artificial limitation that the “to” address cannot be an remote module, the new syntax is useless to our use case. We still need to use the old-fashioned, low-level, error-prone, and impossible-to-peer-review “terraform state mv” method.

We really do hope that there is an option to override this artificial limitation, especially in a use case like ours - the remote module is hosted in a TFE private registry and its maintainer and caller are the same person/team.

Thanks for all your feedback. We plan to allow cross-package move statements in the upcoming v1.3.0 release.

2 Likes