While drafting this RFC originally I had intended to carve out an exception of ignoring required_version arguments in .tf files while continuing to support them in .tofu files, but apparently I lost that detail during some copyediting and so the current draft implies that OpenTofu would continue to use required_version in .tf files unless there's an OpenTofu-specific declaration that takes precedence. This update aims to clarify the proposal's handling of modules that are written only for Terraform without using any OpenTofu-specific mechanisms: in that case, we must just make a best effort to load the module in OpenTofu and let it fail with a more specific error if the module happens to be using language features that OpenTofu does not support, so that loading can succeed when the module is only using the subset of features that are cross-compatible between both systems. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
26 KiB
Miscellaneous Configuration Settings in Modules
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:
- opentofu/opentofu#1708: Needs OpenTofu image version compatible with terraform version 1.7.x
- opentofu/opentofu#3061:
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_versionargument.Because both Terraform and OpenTofu consume this setting and assume it applies to that software, it currently requires weird workarounds (using
.tofufiles 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_providersblock.This block type actually deals with a number of different concerns all at once:
- Declaring which providers the module depends on using the (assumed-)global provider source address namespace.
- Declaring which versions of each provider the module is expected to be compatible with.
- Declaring a module-specific mapping from "local names" to full provider source addresses, such as declaring that in a particular module
awsis short forregistry.opentofu.org/opentofu/aws. - Declaring which provider configuration addresses the module expects to have populated by its caller, using the
providersargument in the callingmoduleblock, instead of by declaring those providers inline in the module.
-
Declaring which variant of the OpenTofu language the module was written for, using the
languageandexperimentsarguments.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
backendorcloudblock 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_metablock.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.
Language and Runtime Versions
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:
- Placing it in a
versions.tffile (or any other.tffile) means that the module will not work in Terraform unless something changes in a future Terraform version to make this work. - For modules that intend to be cross-compatible with Terraform, authors can
create a
versions.tffile containing a Terraform-styleterraformblock withrequired_version, and aversions.tofufile containing alanguageblock which OpenTofu will then use in preference to the settings in theversions.tffile.
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.
The existing required_version argument in terraform blocks
Due 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.
Provider Dependencies
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:
- We have discussed returning to a model where each provider configuration is
able to specify a different version of a provider, rather than requiring
all instances of a particular provider to agree on a single version, in
which case the
versionargument would appear in individualproviderblocks instead of in the entries withinrequired_providers. - We have discussed allowing provider instances to be passed around as normal
values of a special new "provider instance" type, instead of the current
model where provider configurations pass between modules via a special
"side-channel", in which case the
configuration_aliasesargument would likely not exist in its current form withinrequired_providers. - We have discussed alternative ways to specify providers, such as writing a command line to execute directly in the configuration or directly specifying that a provider should be installed from a particular physical location instead of using the source address indirection. If we choose to do this then that suggests quite a different structure for describing which provider is "required".
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.
State Storage Configuration
At the time of writing this proposal we are considering various changes to how OpenTofu thinks about state storage, including:
- Allowing state storage implementations to be offered as part of an OpenTofu provider plugin, rather than having to be built in to OpenTofu CLI.
- Having state storage configured somewhere outside of the root module, so that the same root module can be instantiated multiple times with completely independent state storages.
- Allowing different modules within the same configuration to have different state storage settings, rather than requiring everything to be tracked together in a single location.
- Using a more granular storage scheme for state so that it's no longer stored as just a single huge snapshot that must always be updated as a unit, so that it's possible to work on changes to different parts of the configuration in separate plan/apply rounds without one necessarily invalidating the other.
- Making "remote operations" be something handled by separate tools, rather than built in to OpenTofu.
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.
"Provider Metadata"
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, with the
equinix/equinixprovider supportingmodule_namemetadata that is used by a number of modules published in theequinix-labsGitHub organization. - Google Cloud Platform, with the
hashicorp/googleprovider and thehashicorp/google-betaprovider both supportingmodule_namemetdata that is used by various modules in theGoogleCloudPlatformGitHub organization, and also in forks of those modules. - HashiCorp Cloud Platform, with the
hashicorp/hcpprovider supportingmodule_namemetadata used by various modules in thehashicorpGitHub 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.
Technical Approach
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
opentofuargument in acompatible_withblock populates theCoreVersionConstraintsfield. (All other arguments in this block are completely ignored by OpenTofu, so that other tools can use them without conflict.) -
The
experimentsargument populates theActiveExperimentsfield. -
The
editionargument is treated the same as we currently treat thelanguageargument in aterraformblock, 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.
Open Questions
-
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
.tfand.tofufiles 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
languageblock type with only thecompatible_withblock 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_withare 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 usecompatible_withto exclude OpenTofu versions that do not support the new arguments.There is also a potential compromise in supporting the
experimentsargument as an alias for the existing argument of the same name but not includingeditionat all. There is already existing logic in OpenTofu to handleexperiments, 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
opentofuin acompatible_withblock 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.)
Future Considerations
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_providersblock. -
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.
Potential Alternatives
-
We have previously considered simply supporting
tofuas an alias block type name forterraform, 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
tofuas an alias forterraformas in the previous item, also change the interpretation of therequired_versionblock to assume thatrequired_versionin aterraformblock 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.