rfc: Static Evaluation of Provider Iteration further updates

This continues the work of the last few commits, updating this RFC to
reflect the evolved design that's makes room for adding fully-dynamic
provider instance expansion in a later release.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
Martin Atkins 2024-10-30 17:01:21 -07:00
parent b8d4b24964
commit e6ca786e09

View File

@ -73,7 +73,7 @@ What happens if you use `for_each` without `alias`? That would presumably cause
Therefore we retain the current assumption that a default provider configuration (one without "alias") is always a singleton, and so `for_each` can only be used when `alias` is also set and the instance keys then appear as an extra dynamic index segment at the end of the provider reference syntax. The concept of "default provider configuration" exists in OpenTofu to allow for automatic selection of the provider for a resource in simple cases, and those automatic behaviors rely on the default provider configuration being a singleton.
In the longer term we might allow fully-dynamic expansion and references similar to the existing `for_each` support for resources and module calls, but in prototyping so far we've learned that requires quite significant and risky changes to the core language runtime. This RFC therefore proposes an intermediate step which relies on the ideas set forth in the [Static Evaluation RFC](20240513-static-evaluation.md), which means that the `for_each` argument in a `provider` block and the dynamic index part of a provider reference in a `provider` argument of a resource will initially allow only values known during configuration processing: local values and input variables. Anything dynamic like resources/data will be forbidden for now.
In the longer term we might allow fully-dynamic expansion and references similar to the existing `for_each` support for resources and module calls, but in prototyping so far we've learned that requires quite significant and risky changes to the core language runtime. This RFC therefore proposes an intermediate step which relies on the ideas set forth in the [Static Evaluation RFC](20240513-static-evaluation.md), which means that the `for_each` argument in a `provider` block will initially allow only values known during early evaluation: input variables and local values that are derived from them. Anything dynamic, like a reference to a data resource, remains forbidden for now.
With the provider references clarified, we can now use the providers defined above in resources:
@ -98,13 +98,13 @@ module "mod" {
The `provider` argument for `resource`/`data` blocks and the `providers` argument for `module` blocks retains much of the rigid static structure previously required, to ensure that it remains possible to statically recognize the relationships between resource configuration blocks and provider configuration blocks since that would be required for a later fully-dynamic version of this feature implemented in the main language runtime: it must be able to construct the dependency graph before evaluating any expressions.
However, the new "index" portion delimited by square brackets is, from a syntax perspective, allowed to contain any valid HCL expression. The only constraints on that expression are on what it is derived from and on its result: it must be derived only from values known during planning, its result must be a value that convert to the type `string`, and the string after conversion must match one of the instance keys of the indicated provider configuration.
However, the new "index" portion delimited by square brackets is, from a syntax perspective, allowed to contain any valid HCL expression. The only constraints on that expression are on what it is derived from and on its result: it must be derived only from values known during planning, its result must be a value that can convert to `string`, and the string after conversion must match one of the instance keys of the indicated provider configuration.
Note that in particular this design forces all of the instances of a resource to refer to instances of the same provider configuration, although they are each allowed to refer to a different instance. Again, this is a forward-looking constraint to ensure that it remains possible to build a dynamic evaluation dependency graph where each `resource`/`data` block depends on the appropriate `provider` block before dynamic expression evaluation is possible, even though that isn't strictly required for the static-eval-only implementation.
#### Provider Alias Mappings
Now that we can reference providers via variables, how should this interact with for_each / count in resources and modules?
Now that we can reference providers via variables, how should this interact with `for_each` / `count` in resources and modules?
```hcl
locals {
@ -138,8 +138,125 @@ module "mod" {
This use of `each.key` is familiar from its existing use in `resource`, `data`, and `module` blocks that use `for_each`. The bracketed portion of the address is evaluated dynamically by the main language runtime, and so (unlike the `for_each` argument in `provider` blocks) this particular expression is _not_ constrained to only static-eval-compatible symbols.
From a user perspective, this provider mapping is fairly simple to understand. As we will show in the Technical Approach, this will be quite difficult to implement.
#### Multi-instance provider configurations in tests
The test scenario language (`.tftest.hcl`/etc files) includes three features that affect the treatment of provider configurations:
- [test-scenario-level `provider` blocks](https://opentofu.org/docs/cli/commands/test/#the-providers-block) allow a test scenario to use a different configuration for a provider than would be used normally, while still using the "real" provider plugin.
- [`mock_provider` blocks](https://opentofu.org/docs/cli/commands/test/#the-mock_provider-blocks) allow replacing a "real" provider plugin with an inert mock implemented inside OpenTofu itself, so that the provider configurations are effectively unused.
- The `providers` argument in a [`run` block](https://opentofu.org/docs/cli/commands/test/#the-run-block) changes which provider configurations are bound to which module-local provider configuration addresses in the root module during that test run.
All of these features currently assume that each provider configuration has exactly one instance. That constraint does not hold when testing a module that uses `for_each` in a provider block, and so these features will also change as follows:
* Test-scenario-level `provider` blocks and `mock_provider` blocks are effectively substitutes for provider configurations in the module under test.
These must both therefore also support `for_each` with the same meaning as in `provider` blocks in the main OpenTofu language.
Additionally, the presence of `for_each` _must_ be consistent with the corresponding configuration block in the module under test: if the module itself declares a `provider` block with `for_each` set then any `provider` or `mock_provider` block in a test scenario that uses the same `alias` _must_ include a `for_each` argument. Conversely, if the main module's configuration block _does not_ include `for_each` then the corresponding test scenario configuration must not have one.
A test-scenario-level `provider` or `mock_provider` block whose address matches a `configuration_aliases` entry in the `required_providers` block of a module under test -- that is, the situation where the module expects its caller to _pass in_ a provider configuration and so the test-scenerio-level block is acting as a substitute for that passed-in provider configuaration -- also must not include a `for_each` argument, because in the first iteration of these features described by this proposal it is not yet allowed to pass collections of provider instances between modules.
A test-scenario-level block that doesn't correspond to one of an actual `provider` block in the module under test, an _implied_ empty `provider` block in the module under test, or a `configuration_aliases` entry is a test-scenario-only provider configuration used in conjunction with the `providers` argument in a `run` block. Those are currently _always_ forbidden from using `for_each`.
* The `providers` argument in a `run` block should ideally support the same kinds of provider mappings that the argument of the same name in a `module` block supports, since this argument is intended to behave as if the `run` block is a `module` block calling the module under test.
However, to reduce scope for the initial iteration of this feature we will compromise:
- As with `providers` in a module call, it's invalid to refer to a multi-instance provider configuration without including an instance key, BUT...
- It's also initially forbidden to _include_ an instance key too, because it's not yet clear how to implement that without significant changes to the design of the test harness. This corresponds to the rule that a test-scenario-only provider configuration may not use `for_each`, since the `providers` argument exists primarily to force substituting a test-scenario-only provider configuration for a particular test run and our rule against using `for_each` on those means that there's never any need to specify an instance key.
For example, consider a module under test written as follows:
```hcl
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
variable "aws_regions" {
type = map(object({
enabled = optional(bool, true)
}))
}
provider "aws" {
alias = "by_region"
for_each = var.aws_regions
region = each.key
}
resource "aws_instance" "example" {
for_each = {
for k, v in var.aws_regions : k => v
if v.enabled
}
# ...
}
```
If a test scenario for this module needed to use a mock to replace all uses of the "real" `hashicorp/aws` provider, the `mock_provider` blocks _must_ use the same `alias` and _must_ use `for_each` to match the `provider` block in the main module:
```hcl
# This test scenario verifies that it's possible to perform a two-step
# removal of a region by first disabling it (causing all of the resource
# instances to be destroyed) and then finally removing it.
mock_provider "aws" {
alias = "by_region"
for_each = var.aws_regions
# NOTE: In this example for_each is set to exactly the same expression as
# the corresponding block in the main module, but that's not actually
# required: it would be valid to specify only a subset of regions or
# even a _fake_ set of regions since the mock provider doesn't actually
# know what an "AWS region" even is.
# However, this particular module _does_ expect to have one instance
# of the provider for each entry in var.aws_regions, so we must match
# that here to ensure that we're testing something realistic.
# ...all the existing mock provider settings, unchanged...
}
run "initial_create" {
variables {
aws_regions = {
faked-region-a = {}
faked-region-b = {}
}
}
}
run "disable_b" {
variables {
aws_regions = {
faked-region-a = {}
faked-region-b = {
# Must first disable this region, causing all of the
# resource instances in it to be destroyed before we
# remove the provider configuration that is responsible
# for destroying them.
#
# Failure at this step suggests that there's at least one
# resource directly using for_each = var.aws_regions,
# rather than a filtered version that only includes
# the enabled regions.
enabled = false
}
}
}
}
run "remove_b" {
variables {
aws_regions = {
faked-region-a = {}
# faked-region-b is now completely removed, so it's
# (now-inert) provider configuration is not declared at all.
}
}
}
```
#### What's not currently allowed
@ -185,12 +302,13 @@ resource "example_thing" "example" {
# of the instances of this resource _must_ belong to the one
# single provider configuration block above.
# This is valid as "example.foo" is statically known
# The following would be valid because "example.foo" is statically known...
provider = example.foo[each.key]
# Whereas something like this is not supported as it could refer to
# many potential providers depending on the result of local.alias
provider = example[local.alias][each.key]
# ...but something like the following is not supported as it could refer
# to many potential provider configurations depending on the result of
# local.alias:
# provider = example[local.alias][each.key]
}
```
@ -218,15 +336,15 @@ module "example" {
# However, if we add uncertainty to which provider configuration is
# being referenced, the above assertions do not hold and become much
# more complex to reason about.
example = example[local.alias][each.key] # Unsupported
# more complex to reason about:
# example = example[local.alias][each.key] # Unsupported
}
}
```
We should be able to loosen this restriction in a future RFC, either as part of a fully-dynamic design where provider references are normal expressions as described in the previous section or via some specialized syntax that's only used for provider configurations. The main requirement, regardless of how the surface syntax is designed, is that the graph builder change to support creating multiple dependency edges from a single resource node to multiple provider configuration nodes, so that evaluation of the resource node is delayed until after all of the possibly-referred-to provider configurations have been visited and thus their provider instances are running.
##### Passing collections of provider instances between modules.
##### Passing collections of provider instances between modules
There was previously no syntax for causing a single provider configuration to produce multiple provider instances, and so we also have no existing syntax to use to differentiate between a reference to one provider instance vs. a reference to a collection of provider instances.
@ -259,7 +377,7 @@ module "child" {
}
```
The consequences of `example.foo` being invalid don't really matter for the `resource` block because it doesn't make sense to specify multiple provider instances for a single resource anyway.
The consequences of `example.foo` being invalid don't really matter for a `resource` block because it doesn't make sense to specify multiple provider instances for a single resource anyway.
It _would_ be useful to be able to pass a collection of provider instances to a child module as shown in the second example, but that raises various new design questions about how the child module should declare that it's expecting to be passed a collection of provider instances, since today provider instance references are not normal values and so it isn't really meaningful to describe "a map of `example` provider instances".
@ -278,7 +396,13 @@ Leaving this unsupported for the first release again gives us room to consider v
# a provider "example" block with alias = "foo" and a
# for_each argument, specifying that this module expects its
# caller to provide a collection of provider instances.
example.foo[*]
example.foo[*],
# ...but this syntax does not allow to differentiate a
# collection keyed by strings vs. a collection keyed by
# integers, and so adopting this syntax might block the
# later addition of "count" argument support for provider
# configurations.
]
}
}
@ -308,7 +432,10 @@ Leaving this unsupported for the first release again gives us room to consider v
variable "aws_regions" {
type = map(object({
cidr_block = string
provider_instance = providerinst(aws)
provider_instance = providerinstance(aws)
# (the above is hypothetical new type constraint syntax for
# "a reference to an instance of whichever provider has the
# local name "aws" in this module.)
}))
}
@ -322,12 +449,17 @@ Leaving this unsupported for the first release again gives us room to consider v
```hcl
module "example" {
# (this is a call to the module in the previous code block)
# ...
aws_regions = {
us-west-2 = {
cidr_block = "10.1.0.0/16"
provider_instance = provider.aws.usw2
# (the above is hypothetical new syntax for referring to a
# provider instance in normal expression context, where
# the "provider." prefix differentiates it from being
# a reference to a managed resource.)
}
eu-west-1 = {
cidr_block = "10.2.0.0/16"
@ -353,39 +485,38 @@ Because of how crucial it is to preserve the relationships between resource inst
The following describes the high level changes. A potential implementation has been proposed here: https://github.com/opentofu/opentofu/pull/2105
Our goal is to implement this feature with as little risk as possible. We prioritize minimizing the feature and changeset to minimize the risk. Additional refactoring will likely be performed as this feature grows over time and we choose to implement the most clearly defined and useful parts first.
Our goal is to implement this feature with as little risk as possible. We prioritize minimizing the features and the changeset to minimize the risk. Additional refactoring will likely be performed as this feature grows over time and we choose to implement the most clearly defined and useful parts first.
The core of the changes needed to implement what is described above:
* Allow for_each in provider configuration blocks and evaluate them in the static context
* Initialize and configure a provider instance on every `for_each` entry (exec binary + pass configuration)
* Allow `for_each` in provider configuration blocks and evaluate them in the static context
* Initialize and configure a provider instance for each element of the `for_each` collection (exec binary + pass configuration)
* Allow provider key expressions in `resource > provider` and `module > providers` configuration blocks
* Evaluate the provider key expressions when needed during the evaluation graph to determine which provider instance a resource should use.
* Update state storage to understand per-resource-instance provider keys.
* Evaluate the provider key expressions when needed during the evaluation graph to determine which provider instance each resource instance should use
* Update state storage to understand per-resource-instance provider keys
#### Provider Configuration Blocks
A "provider configuration" is the `provider "type" {}` block in the OpenTofu Language. This is located within either the root module or a child module. Provider configurations can not be declared in child modules which have been invoked with for_each and count.
A "provider configuration" is the `provider "type" {}` block in the OpenTofu Language. This is located within either the root module or a child module. Provider configurations cannot be declared in child modules which have been called with `for_each` or `count`.
Each `provider` block in the configuration is decoded into a `configs.Provider` struct and added to it's respective `configs.Module` at the correct location within the module tree. The existence of this provider block is heavily used when validating the providers within the module tree, see `configs/provider_validation.go`.
Each `provider` block in the configuration is decoded into an instance of the `configs.Provider` struct type and added to it's respective `configs.Module` at the correct location within the module tree. The existence of this provider block is heavily used when validating the providers within the module tree, see `configs/provider_validation.go`.
Each provider block will need to contain it's static Expansion information. "Expansion" is the process of deciding the set of zero or more instance keys that are declared for an object using either `count` or `for_each`. For this initial iteration of the feature, the `for_each` expression will be evaluated as part of the main config loader using the static evaluation context as defined in [Init-time static evaluation of constant variables and locals](https://github.com/opentofu/opentofu/blob/main/rfc/20240513-static-evaluation.md).
Each provider block will need to contain it's static expansion information. "Expansion" is the process of deciding the set of zero or more instance keys that are declared for an object using either `count` or `for_each`. For this initial iteration of the feature, the `for_each` expression will be evaluated as part of the main config loader using the static evaluation context as defined in [Init-time static evaluation of constant variables and locals](./20240513-static-evaluation.md).
The `configs.Provider` struct representing each configured `provider` block will now contain new field that contains the static mapping from "provider instance key" to "provider repetition data". In practice, this is a map of `addrs.InstanceKey` to `instances.RepetitionData`. This map can be built using the StaticContext available in [configs.NewModule()](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/configs/module.go#L122) as defined in the Static Evaluation RFC.
The `configs.Provider` struct representing each configured `provider` block will now contain new field that contains the static mapping from "provider instance key" to "provider repetition data". In practice, this is a `map[addrs.InstanceKey]instances.RepetitionData` value. This map can be built using the StaticContext available in [configs.NewModule()](https://github.com/opentofu/opentofu/blob/290fbd66d3f95d3fa413534c4d5e14ef7d95ea2e/internal/configs/module.go#L122) as defined in the Static Evaluation RFC.
At the end of the `configs.NewModule` constructor, all provider configurations that contain a for_each or count will have their new "instance mapping" field set (or an error message produced). This should not change the majority of provider validation logic in the configuration package as the name/type/alias information *has not changed*.
At the end of the `configs.NewModule` constructor, all provider configurations that contain a `for_each` argument will have their new "instance mapping" field set (or an error message produced). This should not change the majority of provider validation logic in the configuration package as the name/type/alias information _has not_ changed.
##### Provider Node Execution
Each "provider configuration block" is turned into a "provider node" (NodeApplyableProvider) within the tofu graph. When a provider node is executed, it starts up and configures a `providers.Interface` of the corresponding provider. This "interface" is stored in the `tofu.EvalContext` and is available for use by other nodes via the context (referenced below).
`NodeApplyableProvider` now effectively represents all `provider.Interface`s of a particular `provider` configuration block at once, and its "execute" step now involves a loop performing for each instance similar behavior that was previously always singleton:
`NodeApplyableProvider` now effectively represents all `provider.Interface`s of a particular `provider` configuration block at once, and its "execute" step now involves a loop performing for each instance the behavior that was previously always singleton:
1. Evaluate the configuration using the appropriate `instances.RepetitionData`, thereby causing `each.key` and `each.value` to be available with instance-specific values. The result is a `cty.Value` representing an object of the type implied by the provider's configuration schema.
2. Create a child process running the appropriate provider plugin executable, creating a running provider interface.
3. Call the `ConfigureProvider` function from the provider protocol on the new provider interface, passing it the `cty.Value` representing its instance-specific configuration.
4. Save the provider interface in the shared `tofu.EvalContext` for use by downstream resource nodes.
A special case exists here for `tofu validate`. The validate codepath does not configure any providers or deal with module/resource "expansion" (for_each/count). A validate graph walk should only initialize and register one `providers.Interface`. It can still however validate provider configurations using the provider's schema and `providers.Interface.ValidateProviderConfiguration` call for each set of provider instance data on the single instance.
A special case exists here for `tofu validate`. The validate codepath does not configure any providers or deal with module/resource "expansion" (`for_each`/`count`). A validate graph walk should only initialize and register one `providers.Interface`. It can still however validate provider configurations using the provider's schema and `providers.Interface.ValidateProviderConfiguration` call for each set of provider instance data on the single instance.
##### Selecting a provider instance for each resource instance
@ -406,7 +537,7 @@ Each resource _instance_ must also now track the final `addrs.InstanceKey` that
##### Dynamic provider instance keys in `module` blocks
When the dynamic provider instance selection occurs in a `module` block, rather than directly in a `resource`/`data` block, it appears as part of the existing capability of projecting some or all of the provider configurations in the calling module to also appear (potentially with different aliases) in the callee. For example:
When the dynamic provider instance selection occurs in a `module` block, rather than directly in a `resource`/`data` block, it participates in the existing capability of projecting some or all of the provider configurations in the calling module to also appear (potentially with different aliases) in the callee. For example:
```hcl
provider "aws" {
@ -437,24 +568,24 @@ At the end of this process, each resource node should know both what "provider c
##### Resource Instance Visiting/Execution
When a resource instance is visited (Execute method called), it's first task is to determine which provider it should be using.
When a resource instance is visited (`Execute` method called), it's first task is to determine which provider it should be using.
With all of the previously described machinery in place, it has access to:
* The required provider configuration block address (ProviderTransformer)
* The module level provider key expression + module path (ProviderTransformer/ProviderConfigTransformer)
* The resource provider key expression (configs.Resource)
* The required provider configuration block address (`ProviderTransformer`)
* The module level provider key expression + module path (`ProviderTransformer`/`ProviderConfigTransformer`)
* The resource provider key expression (`configs.Resource`)
The resource instance then determines if it should be using the module or resource provider key expression (only one is allowed). It evaluates that expression within the specified context and produces a "provider instance key".
It then asks the EvalContext for the `providers.Interface` determined by "provider configuration block address" and "provider instance key".
It then asks the EvalContext for the `providers.Interface` using the provider configuration block address and the provider instance key, which together represent a fully-qualified provider instance address.
Once the provider interface has been determined (resolved), it can continue it's usual unmodified execution path.
Once the provider instance has been selected, it can continue it's usual unmodified execution path.
##### Providers Instances in the State
Currently OpenTofu assumes that all instances of a resource must be bound to the same provider instance, because each provider configuration can have only one instance and each resource can be bound to only one provider configuration. This assumption is unfortunately exposed in the state snapshot format, which tracks the provider configuration address as a property of the overall resource rather than of each instance separately.
To allow for future generalization without the need to make further breaking changes to the state snapshot format, and to avoid making rollback to an older OpenTofu version difficult for anyone who has never used these new features, we will extend the state snapshot format to _also_ track provider instance information at the resource instance level.
To allow for future generalization without the need to make further breaking changes to the state snapshot format, and to avoid making rollback to an older OpenTofu version difficult for anyone who has never used these new features, we will extend the state snapshot format to also track full provider instance information at the resource instance level only for resources whose provider instance selection uses a dynamic instance key.
Today's state snapshot format includes the following information for each resource (irrelevant properties excluded):
@ -477,7 +608,7 @@ Today's state snapshot format includes the following information for each resour
}
```
We will add a similar `"provider"` property to each of the objects under `"instances"` which tracks the same information but might additionally capture a trailing instance key for the specific selected provider instance:
We will mode the `"provider"` property to each of the objects under `"instances"` which tracks the same information, with the addition of a trailing instance key for the specific selected provider instance:
```json
{
@ -499,12 +630,31 @@ We will add a similar `"provider"` property to each of the objects under `"insta
}
```
The state snapshot loader will support both the old and new forms. If the resource-level `"provider"` property is present then any instance that does not have a `"provider"` property will have the value from the resource-level property copied into it, causing all of those instances to therefore refer to the same singleton provider configuration for the same meaning as before.
After potentially propagating the resource-level addresses into instances that didn't have such a property, the state snapshot loader will verify that all of the instances have provider instance addresses that differ _only_ in the trailing instance key, and will fail with an error if not. That constraint preserves for now the fundamental assumption in the language runtime that each resource depends on exactly one provider configuration, while retaining the freedom to loosen that constraint in later versions without further changes to the state snapshot syntax and thus without the need for additional format migration code.
The current version of [the OpenTofu v1.x Compatibility Promises](https://opentofu.org/docs/language/v1-compatibility-promises/) at the time of writing contains the following statements:
> You should be able to upgrade from any v1.x release to any later v1.x release. You might also be able to downgrade to an earlier v1.x release, but that isn't guaranteed: later releases may introduce new features that earlier versions cannot understand, including new storage formats for OpenTofu state snapshots.
An older version of OpenTofu would not be able to successfully load a state snapshot without a resource-level `"provider"` property, which would therefore be an example of "new features that earlier versions cannot understand". However, we try to avoid blocking downgrades whenever possible notwithstanding the above statements, because downgrading back to an earlier version shortly after upgrading can be a helpful workaround for newly-introduced bugs.
As a compromise then, the state snapshot _writer_ will generate instance-level `"provider"` properties only if there's at least one instance with a different provider instance address than the others. If all instances of a particular resource have the same provider instance address then the writer will instead generate a resource-level `"provider"` property containing that single provider instance address. This means that any configuration that is not using the new features proposed in this RFC will continue to generate state snapshots that are backward-compatible with previous versions of OpenTofu (notwithstanding any other unrelated changes to the state snapshot format outside the scope of this RFC).
The state snapshot loader will parse both the resource-level address and the instance-level addresses and determine which to use for each resource instance. It will, for now, return an error if any of the instance addresses are not identical aside from the optional trailing instance key. This preserves our current simplification of each resource still being bound to exactly one provider configuration while offering a more general syntax that we can retain in future if we weaken that constraint. We don't yet know exactly what flexibility we will need for future features, and so capturing the entire provider instance address (and then verifying their consistency) gives us the freedom to refer to literally any valid provider instance address in future versions, without changing the syntax and thus without the need for new state snapshot format upgrade logic.
We could retain the `"provider"` argument at the resource level, despite it now being technically redundant. It would offer a measure of backward-compatibility with older versions of OpenTofu. When the core team discussed this in depth, we decided that it would likely be better to omit this duplicate information when possible to force the user to roll-back and apply their configuration before downgrading their OpenTofu version. This is not a supported path, but we want to make sure that the proper steps are taken when downgrading.
This section described changes only to the internal state snapshot format that is not documented for external use. We do not intend to extend the documented `tofu show -json` output formats yet as part of this change, because we want to retain design flexibility for later work and to learn more about what real-world use-cases exist for machine-consumption of the provider instance relationships before we design a final format that will then be subject to compatibilty constraints.
> [!NOTE]
> An earlier version of this document instead proposed that the state snapshot writer would always populate both the resource-level _and_ the instance-level `"provider"` properties.
>
> The main implication of this decision is whether it is likely to be beneficial or harmful for older versions of OpenTofu (and potentially other software parsing the state snapshot format despite it not being subject to compatibility constraints) to be able to still find the resource-level provider configuration address for a resource whose instances each dynamically select an instance of that configuration.
>
> The current proposal text aims for the compromise of intentionally making older versions of OpenTofu _fail_ if there is any resource making use of the new features in this proposal, but to succeed if this feature has not been used.
>
> In particular this means that anyone who wishes to downgrade to an older version of OpenTofu _after_ using these features will need to first rewrite their configuration to remove uses of these features and run `tofu apply` to return the state into a backward-compatible form. If we were to permit older versions of OpenTofu to read a state snapshot generated from use of these features then the older versions would misunderstand the references in the state and fail in a more confusing way, whereas the versions of OpenTofu that support these new features will have explicit support for generating old-style state snapshots whenever that's possible.
### Open Questions
#### Dynamic or early-evaluated provider dependencies
@ -513,33 +663,32 @@ Should variables be allowed in required_providers now or in the future? Could h
```hcl
variable "version" {
type = string
type = string
}
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = var.aws_version
}
required_providers {
aws = {
source = "hashicorp/aws"
version = var.aws_version
}
}
}
```
Initial discussion with Core Team suggests that this should be moved to it's own issue as it would be useful, but does not impact the work done in this issue.
Source address and version selection is primarily an installation-time concern rather than a runtime concern, as long as the `source` argument is always populated consistently. Therefore there is no real interaction between supporting multiple provider instances and supporting programmatically-generated source information, and so the decision about whether and how to support the latter can be left for a later discussion.
#### Dynamic or early-evaluated provider aliases
Early discussion for this feature considered possibly allowing more than just constant strings in the `alias` argument. If we supported this it would likely be limited only to early-evaluation to preserve the requirement that each `provider` block have a known alias when we build the main language runtime's dependency graph.
Early discussion for this feature considered possibly allowing more than just constant strings in the `alias` argument. If we supported this it would likely be limited only to early-evaluation to preserve the requirement that each `provider` block have a known alias when we build the main language runtime's dependency graph. For example:
Example:
```hcl
# Why would you want to do this?
provider "aws" {
alias = var.foo
}
```
We have intentionally omitted this for now because we don't know of any real use-cases for it, and it's conceptually easier to imagine `alias` as being effectively equivalent to the second label in the header of a `resource` block -- a statically-chosen unique identifier for use in references elsewhere -- than as another possibly-dynamic element in addition to instance keys.
We have intentionally omitted this for now because we don't know of any real use-cases for it, and it's conceptually simpler to explain `alias` as being effectively equivalent to the second label in the header of a `resource` block -- a statically-chosen unique identifier for use in references elsewhere -- than as another possibly-dynamic element in addition to the dynamic instance keys.
We can safely add early-eval-based aliases in a later release without any breaking changes, so we will await specific use-cases for this before we decide whether and how to support it.