rfc/20250730-module-misc-settings.md
The current OpenTofu language inherited a top-level block type named terraform from its predecessor. Blocks of this type contain an assortment of only-tangentially-related settings that seem to have ended up there just because there wasn't any other obvious place to put them.
This document proposes new alternatives to those settings that are intended to be tool-agnostic, while also making some room for other changes we have already discussed including in future versions of the language.
Relevant issues:
required_engine = "opentofu"The core observation of this proposal is that the current collection of settings that are supported in terraform blocks has no specific theme or rationale for being collected together in this way, and so we can and should reconsider each of those settings in how they relate to each other and to the module they are declared within rather than simply replacing the terraform block type directly with some other block type name.
The terraform block is currently responsible for:
Specifying which versions of Terraform or OpenTofu the module is expected to be compatible with, using the optional required_version argument.
Because both Terraform and OpenTofu consume this setting and assume it applies to that software, it currently requires weird workarounds (using .tofu files that Terraform cannot "see") to declare both a Terraform version requirement and an OpenTofu version requirement in the same module.
Declaring which providers the module requires and which versions of each provider the module is expected to be compatible with, using a required_providers block.
This block type actually deals with a number of different concerns all at once:
aws is short for registry.opentofu.org/opentofu/aws.providers argument in the calling module block, instead of by declaring those providers inline in the module.Declaring which variant of the OpenTofu language the module was written for, using the language and experiments arguments.
Today's OpenTofu does not tend to make use of either of these, because it has only one rolling language edition and does not use language experiments. In principle though, these arguments allow a particular module to opt in both to new editions of the language that might have slight incompatibilities with older editions, and to opt-in to participating in experimental new language features that are not yet subject to compatibility promises.
Configuring a "backend" in a root module, using either the backend or cloud block types.
For modules used as root modules only, these two mutually-exclusive block types can specify where OpenTofu should store state snapshots or even cause OpenTofu CLI to act only as a local terminal to another execution process running on some remote system.
Specifying "provider metadata", using the provider_meta block.
This rarely-used feature is useful only for situations where a module has been developed by the same entity as a provider that module uses, and that vendor wants to use the provider as a vehicle for collecting usage metrics for the module by adding additional information to every request made to the given provider related to resources declared in the module. It has no other reasonable purpose.
This proposal will provide at least a high-level direction for the future of each of these, although the details of some are intentionally left to later RFCs which might also change exactly what information we need to collect on each of these topics.
Although it's difficult to find a meaningful link between all of the settings currently configured in terraform blocks, what several of them have in common is that they describe the related concerns of which version of the language the module is intending to use and what versions of which runtimes (e.g. OpenTofu and Terraform) the module is expected to be compatible with.
Keeping those settings all declared together in a single place is reasonable because they describe different facets of the same concern and so are likely to change together. Therefore we can group these all together in a new top-level block called language, which subsumes what we currently handle with the required_version, language and experiments arguments in terraform blocks:
# The "language" block type essentially describes what OpenTofu language the
# module author was intending to use. Since it's a "living language" we
# don't explicitly version individual changes to it, so talking about which
# versions of OpenTofu the module is compatible with is the main idea.
language {
# compatible_with declares which runtime software the module is known to be
# compatible with, intentionally defined generically so that other software
# can potentially interpret modules written in the OpenTofu language.
compatible_with {
# Only the "opentofu" argument would actually be interpreted by OpenTofu,
# treating it as a version constraint for OpenTofu CLI versions.
opentofu = ">= 1.15"
# Other arguments are allowed in here but are completely ignored by
# OpenTofu. Other software could potentially define its own argument
# name for use in this block, and define what values are valid for
# that argument name.
}
# OpenTofu does not currently use language editions, but reserving an argument
# for specifying them means that if we _do_ later introduce a new one then
# older OpenTofu versions can potentially return a more useful error message
# about it, rather than simply complaining about an invalid argument.
edition = OTF2028
# Again, OpenTofu does not currently use experiments, but defining the argument
# means that we can return errors saying that any specified experiment is
# not available in the current OpenTofu version, rather than returning a
# generic syntax error.
experiments = []
}
These settings can all potentially affect arbitrary details of how OpenTofu interprets the rest of a module, so all of these settings are required to be configured with constant values (no early eval). These settings are effectively describing characteristics of the module itself, rather than the environment where it is being run, and so we accept this compromise to ensure that we could potentially vary even the early evaluation behavior itself based on these settings in future versions of OpenTofu.
Modules that use a language block should choose carefully where to place it:
versions.tf file (or any other .tf file) means that the
module will not work in Terraform unless something changes in a future
Terraform version to make this work.versions.tf file containing a Terraform-style terraform block
with required_version, and a versions.tofu file containing a language
block which OpenTofu will then use in preference to the settings in
the versions.tf file.For any module that contains a language block, OpenTofu will completely
ignore any of the corresponding arguments within terraform blocks assuming
that they are intended only for Terraform's use. We'd recommend, but not
require, that language blocks be placed in .tofu configuration files.
required_version argument in terraform blocksDue to the history of both projects, unfortunately both OpenTofu and Terraform
make use of the required_version argument in a terraform block, but
OpenTofu interprets it as an OpenTofu version number while Terraform interprets
it as a Terraform version number.
There is no particular correspondance between those version numbers after
the v1.5 series where the projects diverged, and so for a cross-compatible
module that needs to mention a newer version in its constraint we currently
recommend that authors create both a .tf file and a .tofu file of the
same basename, and place the Terraform version constraint in the .tf file
and the OpenTofu constraint in the .tofu file.
As part of implementing this proposal, we would slightly change the existing
behavior so that OpenTofu will always completely ignore required_providers
settings in .tf and .tf.json files, assuming that they are intended for
Terraform. OpenTofu will continue to honor required_providers arguments
in .tofu and .tofu.json files, so existing modules already using that
pattern will retain their current meaning.
Module authors that wish to support OpenTofu versions prior to the introduction
of the language block type should continue following the existing pattern.
Authors should adopt a language block and nested compatible_with block
only once the minimum required OpenTofu version is one that supports the new
syntax.
Authors of broadly-shared modules might prefer to delay adopting the new syntax
until all currently-supported OpenTofu minor release series support it, so that
anyone trying to use the module with older versions of OpenTofu will recieve
an error message about an incorrect OpenTofu version, rather than a generic
syntax error about the unsupported language block.
The providers needed for a module are a top-level concern of that module and
so we shall introduce a new top-level required_providers block type instead
of having it nested inside any other block type.
This proposal intentionally leaves the contents of this new block unspecified because there are various other ideas under discussion at the time of writing that would affect its design if accepted:
version argument would appear in individual provider
blocks instead of in the entries within required_providers.configuration_aliases argument would
likely not exist in its current form within required_providers.If this proposal is accepted then the next minor version of OpenTofu should
include support for recognizing a top-level required_providers block and
generating a specialized error message saying that it's reserved for use in
a future version of OpenTofu, so that introducing fully in a later version
of OpenTofu would cause older versions to return an error message that directly
encourages the reader to investigate whether a module they are trying to use
requires a newer version of OpenTofu.
We will continue to rely on the current form of required_providers nested
inside terraform until we have made more progress on the other discussions
that might affect its structure, so that we can introduce a new design built
with the future ideas in mind rather than just copying the existing design
and then potentially having to accept awkward compromises to make later features
work with it.
At the time of writing this proposal we are considering various changes to how OpenTofu thinks about state storage, including:
All of these potentially impose new requirements on the configuration syntax we
use for configuring state storage. Therefore this document does not yet propose
any specific replacement for the current backend and cloud block types
within terraform blocks, except to say that if the new design does include
something similar to the current idea of in-root-module backend configuration
then it should appear as a new top-level block type, not nested inside any other
block type. (It's also possible that a new design would not include any
equivalent of this at all.)
Until those discussions have progressed further and we have a better idea of
what requirements we're trying to design for, OpenTofu authors should continue
using backend or cloud blocks inside terraform blocks, and we will keep
that pattern working in some form in future versions to give authors time
to transition gradually to whatever replaces them.
The provider_meta block is narrowly focused on the relatively unusual case
where a module is maintained by the same vendor that maintains the main provider
it uses. It is not useful in the more common case where a module is written by
a different party than the providers it uses.
Based on a GitHub Code Search, it appears that the only vendors currently making public use of this mechanism are:
equinix/equinix provider
supporting module_name metadata that is used by a number of modules
published in the equinix-labs GitHub organization.hashicorp/google provider
and the hashicorp/google-beta provider
both supporting module_name metdata that is used by various modules in
the GoogleCloudPlatform GitHub organization, and also in forks of those
modules.hashicorp/hcp provider
supporting module_name metadata used by various modules in the hashicorp
GitHub organization.We do not have any intention of breaking existing uses of this, but it's also
not clear at this time whether this mechanism is a good fit for OpenTofu
in particular and whether it would be supported by future provider protocol
versions at all. Therefore this can continue using provider_meta blocks
inside terraform blocks primarily for backward-compatibility, and will defer
introducing any new syntax for it for now.
The initial limited scope described above can be implemented entirely within
OpenTofu's package configs, with no impact on the rest of the system.
No changes to the public API of that package are required. Instead, the new
language block type introduces a new way to populate existing fields
of configs.Module:
The opentofu argument in a compatible_with block populates the
CoreVersionConstraints field. (All other arguments in this block are
completely ignored by OpenTofu, so that other tools can use them without
conflict.)
The experiments argument populates the ActiveExperiments field.
The edition argument is treated the same as we currently treat the
language argument in a terraform block, which is to check whether it's
been set to some fixed token we consider to represent the current OpenTofu
language version and if not to return an error saying this module seems to
be intended for a different version of OpenTofu.
Because there is only one acceptable edition to select, the selection is not currently exposed anywhere in the public API.
The only other change immediately required for this proposal is to recognize
a top-level required_providers block and to immediately return a specialized
error message about it. That can be implemented internally within the
configuration decoding logic and so does not require any public API changes.
Is it okay that the new language-related settings would not be immediately usable for many module authors?
Introducing an entirely new syntax for describing language-related settings means that older versions of OpenTofu will consider any usage of that syntax to be a syntax error, returning an error message that does not clearly suggest that the module might be intended for a newer version of OpenTofu.
This proposal asserts that it's okay to lay the groundwork for a nicer syntax in future, even if that means that many authors would continue to use the existing syntax for some time until they are ready to require a sufficiently-new version of OpenTofu.
The author believes this to be justified because we already have one
cross-compatible solution for presenting different version information to
OpenTofu vs Terraform -- using .tf and .tofu files with the same
basename -- and that will continue to work throughout the transition period
so that authors of existing modules can make their own decision about when
to use the new syntax.
Are we okay with leaving so many design questions unanswered?
This proposal mainly focuses on the general idea of moving away from using any block type that's named after a particular product, while leaving most of the details of that unspecified with the assumption that future RFCs will tackle those questions.
Would we prefer to wait until the other discussions are further along so that we can design this all together as a single unit? Might there be "unknown unknowns" that would cause us to design differently even the small subset that this proposal initially aims to change?
We don't have any particular urgency to change anything right now. We could choose to wait, if we think the risk outweighs the reward.
Should we just ignore language editions and experiments for now?
OpenTofu has never made any use of either of these mechanisms; they are just ideas we inherited from our predecessor. We could decide to leave those existing mechanisms unchanged and not introduce any new syntax for them for now, until we have a better idea of whether and how we might use them in OpenTofu.
This proposal includes them primarily because thematically they seem to
belong to the same category of settings as the OpenTofu runtime version
constraints and so proposing a new location for all of them together
seemed wise. However, we could choose to introduce the new language
block type with only the compatible_with block type to start and
then add other arguments later once we know what problems we're trying
to solve, which would give us more freedom to choose to do something
significantly different than what's stubbed today.
The only slight advantage of doing something for these immediately is that
-- as with the existing language features for these concepts -- we can
introduce real uses of them later knowing that at least some older versions
of OpenTofu recognize them enough to return a specialized error message
about them. However, we could achieve that a different way by ensuring
that the version constraints in compatible_with are always checked
before returning any other errors and then expect that module authors
using whatever hypothetical language-edition-like or experiment-like
features we add will also use compatible_with to exclude OpenTofu
versions that do not support the new arguments.
There is also a potential compromise in supporting the experiments
argument as an alias for the existing argument of the same name but not
including edition at all. There is already existing logic in OpenTofu
to handle experiments, but our only handling of language editions today
is to return an error if the argument is set to anything other than a
placeholder token representing an older version of the Terraform language.
Should we allow modules to specify that they aren't compatible with OpenTofu at all?
The current proposal focuses on the situation where a module is written to support OpenTofu but wants to declare that it's only intended to work with a certain subset of OpenTofu versions.
It does not include any way to assert that the module is not intended to be used with any version of OpenTofu, i.e. that it is intended for use only with Terraform or with some other hypothetical future tool that replaces OpenTofu while still supporting its module format.
We could potentially support a special extra value assigned to opentofu
in a compatible_with block which represents an empty set of compatible
versions, whereas the default when unspecified is the maximum set containing
all possible versions. Is this useful enough to be worth the additional
complexity that implies?
(Technically we could add such a thing later as long as earlier versions of OpenTofu would reject the new syntax as an error, since it would still then have the effect of making the module not work with those older versions of OpenTofu, but with a worse error message. If that worse error message were acceptable then the author could just set the version constraint to any invalid version constraint syntax to get the same effect.)
At the time of writing the following discussions are ongoing, which this proposal is intentionally aiming to leave room for without blocking on their conclusion:
Backends as plugins suggests that some or all of the functionality of what we currently call "backends" -- state storage, at least -- would be implemented in a plugin rather than built in to OpenTofu.
There are various ways that could work and each imposes some different requirements on the configuration syntax for configuring them.
Discussion of a new provider protocol
includes some ideas for different ways to fetch and execute provider plugins,
which might impose new design requirements on the required_providers block.
Scalable Root Modules in OpenTofu is an umbrella issue for various discussion about ways OpenTofu could better support describing and maintaining larger infrastructure estates, which includes possibilities of changing state storage and possibly introducing an additional concept above "root module" to make root modules themselves more reusable.
Backward-compatible Additions of new Reference Symbols discusses a way to avoid introducing a new language edition for certain kinds of otherwise-breaking changes, but does not solve everything and imagines that language editions might still be involved as a way to more clearly aggregate collections of new functionality together once after they've had some time to "bake" using more complicated backward-compatibility mechanisms.
"Stack configuration" files investigates a very different approach to state storage where it's configured outside of the root module.
"Registry in a file" considers a new model where authors are able to get an effect similar to running a private module registry but using only a file distributed alongside the configurations that would make use of it.
We have previously considered simply supporting tofu as an alias block type
name for terraform, while changing nothing else.
That is technically feasible and relatively easy to achieve, but arguably repeats the mistake of using a specific product's name as part of the language, and would squander the opportunity to revisit the design of these nested elements to make room for known future ideas.
Tofu Version Compatibility
previously discussed a more constrained change that would, along with
adopting tofu as an alias for terraform as in the previous item, also
change the interpretation of the required_version block to assume that
required_version in a terraform block is more likely to be talking about
a Terraform version than an OpenTofu version.
This is a narrower solution that does not significantly change the existing texture of the language. However, that makes it potentially harder to explain -- having a language feature of the same name across both Terraform and OpenTofu which is nonetheless interpreted subtly differently in each -- and left unanswered the question of how OpenTofu should react (if at all) to Terraform version constraints.
This new proposal instead leaves the existing language features unchanged, "warts and all", and introduces something separate that module authors can gradually adopt over time.