-
Notifications
You must be signed in to change notification settings - Fork 9.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ability to pass providers to modules in for_each #24476
Comments
Hello! 🤖 This issue seems to be covering the same problem or request as #9448, so we're going to close it just to consolidate the discussion over there. Thanks! |
Uh. These requests are in no way similar. Bad bot. |
Yeah, this is not the same as #9448 at all. @pselle @apparentlymart could you help with reopening this issue? |
Hey there @mightyguava & @jspiro, I'm going to re-open the issue as I agree that the concerns are not the same. I did rename it for clarity; to distinguish this request from instantiating providers with |
@mightyguava .
├── dev
│ └── terragrunt.hcl
├── modules
│ └── my_module
│ └── main.tf
├── prod
│ └── terragrunt.hcl
├── stage
│ └── terragrunt.hcl
└── variables.tf
terraform {
source = "${get_parent_terragrunt_dir()}/../"
}
# will generate content to ./providers.tf
generate "providers" {
path = "providers.tf"
if_exists = "overwrite"
contents = <<EOF
provider "azurerm" {
# my main provider
version = "~> 2.6"
subscription_id = "11111111-2222-3333-4444-555555555555"
client_id = var.client_id
client_secret = var.client_secret
tenant_id = var.tenant_id
features {}
}
provider "azurerm" {
alias = "my_alias_provider"
version = "~> 2.6"
subscription_id = "66666666-7777-8888-9999-000000000000"
client_id = var.client_id
client_secret = var.client_secret
tenant_id = var.tenant_id
features {}
}
EOF
}
# will generate content to ./main.tf
generate "main" {
path = "main.tf"
if_exists = "overwrite"
contents = <<EOF
module "my_main_provider" {
source = "./modules/my_module"
}
module "my_alias_provider" {
source = "./modules/my_module"
providers = {
azurerm = azurerm.my_alias_provider
}
}
EOF
}
# passing these from cli or exporting to TF_VAR
# Note that both of my subscriptions use the same SP for auth and
# both in the same tenant, so the difference is only the subscription_id
variable "client_id" {}
variable "client_secret" {}
variable "tenant_id" {} The Maybe this is not fully covering your scenario, but it provides some flexibility over different environment settings |
First of all, thanks for the great work adding iteration and depends_on for modules - both are going to be really useful and I wished for them so many times back during 0.11 days when we were building the majority of our config. In addition to each.key, I'd expect to be able to freely use maps with for_each and have each.<property> be a provider. This would require the ability to assign a provider "instance" to a local or list/map members.
This was the first thing I tried to do when I learned that 0.13 has for_each for modules, which brought me to #17519 and eventually - here. Since we maintain infrastructure in multiple AWS regions and availability zones around the world, most of the modules in our configuration require passing a provider along with at least a few other variables. |
While not strictly the same as #9448 I think they might be solved together. First, like @vivanov-dp said, thanks for adding the However I had not realized that provider configuration in modules was deprecated. Here is my use case:
What I have now is that all those standard resources are in a module. I instanciate the module once per sub account and I pass the IAM role to the module. The module then opens a provider connection to the right account and the right role (different for each module instance). This still works in 0.13. However, when I tried to migrate to "for_each" to instanciated all the modules for all the sub-account in a single module block, I hit the issue that providers inside modules are not supported anymore. And since I can't construct a dynamic list of providers, I think I'm stuck. Am I right to think there is currently no workaround for my use case? Should I then split account creation and account "basic provisionning" in 2 different terraform projects? thanks |
I personally think that inline provider declaration, which honors the module
Ideally, this would support Another option is to add
|
@nikolay |
@vivanov-dp This was pseudocode just to illustrate my point, which was that the logic of how the provider should be initialized could be encapsulated in the module. I can't think of a situation where the instantiation of a provider would be an expensive operation. Also, providers with |
@nikolay
instead of:
But then I have one set of providers for everything else and one set of the same properties just for the modules. So which one is the source of truth ? Unless I declare my providers by using the same sets of properties - so I have to create a new abstraction - the set of providers properties and use that in all places. As I said - it can do the job, I think it looks nice and is not a bad idea, but IMO it involves more changes to the existing configuration than if we could just use the already defined providers - which in my case are in an external file, often 1 or 2 directories up the hierarchy and propagated down via a script that generates |
A use case for me would be to configure a dynamic provider based on output from a module using such as creating multiple kubernetes clusters ( module "foo" {
source = "./foo"
for_each = var.foo_things
var1 = each.key
var2 = each.values.something
}
module "bar" {
source = "./bar"
for_each = { for k, v in var.bar_things : k => v if v.add_bar_to_foo == true }
provider "some_provider" {
config1 = module.foo[each.values.foo_thing].output1
config2 = module.foo[each.values.foo_thing].output2
config3 = module.foo[each.values.foo_thing].output3
}
var1 = each.key
var2 = each.values.something
} |
@vivanov-dp The ideal approach is to have identical code and only data, which varies between environment and clusters within the environment. Right now, almost everything has |
@jon-walton Your example is identical to mine, but I illustrated if the module needs more than a single provider. |
My example illustrates the provider config being supplied to a module is set by the output of another module which also uses |
@nikolay Sure, having |
@jon-walton Fair enough, we need dynamic providers - one way or another. Right now providers and outputs are the only two static resources in Terraform. |
Hi all! Thanks for the interesting discussion here. It feels to me that both this issue and #9448 are covering the same underlying use-case, which I would describe as: the ability to dynamically declare and use zero or more provider configurations based on data determined at runtime. These various proposals all have in common a single underlying design constraint: unlike most other concepts in Terraform, provider configurations must be available for operations on resources that belong to them, which includes planning, updating, and eventually destroying. This means that a provider configuration must be available at the same time a new resource is added to the configuration, must have a stable name that can be tracked between runs in the Terraform state, and they must continue to be available until every resource instance belonging to them has been destroyed and/or removed from the state. It is due to that design constraint that provider configurations remain separated from all other concepts in the restrictions placed on them in the configuration. Design work so far seems to suggest that there are some paths forward to making provider configuration associations (that is, the association of resources to provider configurations) more dynamic, but the requirement that each provider configuration be defined by a static One design we've considered (though this is not necessarily the final design we'd move forward with) is to make provider configurations a special kind of value in the language, which can be passed by reference through expressions in a similar sense that other values can. For example: variable "networks" {
type = map(
object({
cidr_block = string
aws_provider = providerconfig(aws)
})
)
}
resource "aws_vpc" "example" {
for_each = var.networks
provider = each.value.aws_provider
cidr_block = each.value.cidr_block
} The However, the calling module would still be required to declare the provider configurations statically with provider "aws" {
alias = "usw2"
region = "us-west-2"
}
provider "aws" {
alias = "use2"
region = "us-east-2"
}
module "example" {
source = "./modules/example"
networks = {
usw2 = {
cidr_block = "10.1.0.0/16"
aws_provider = provider.aws.usw2
}
use2 = {
cidr_block = "10.2.0.0/16"
aws_provider = provider.aws.use2
}
}
} Notice that the two provider configurations must still have separate static identities, which can be saved in the state to remember which resource belongs to which. But this new capability of sending dynamic references for provider configurations still allows writing a shared module that can be generic in the number of provider configurations it works with; only the root module is required to retain a static set of provider configurations. There is also some possibility here of allowing I've shared the above mainly to just show some initial design work that happened for this family of features. However, I do have to be honest and share some unfortunate news: the focus of our work is now shifting towards stabilizing Terraform's current featureset (with minor modifications where necessary) in preparation for a Terraform 1.0, and a mechanism like the one I described above would be too disruptive to Terraform's internal design to arrive before that point. The practical upshot of this is that further work on this feature couldn't begin until at least after Terraform 1.0 is released. Being realistic about what other work likely confronts us even after the 1.0 release, I'm going to hazard a guess that it will be at least a year before we'd be able to begin detailed design and implementation work for features in this family. I understand that this is not happy news: I want this feature at least as much as you all do, but with finite resources and conflicting priorities we must unfortunately make some hard tradeoffs. I strongly believe that there is a technical design to address the use-cases discussed here, but I also want to be candid with you all about the timeline so that you can set your expectations accordingly. |
This can be accomplished by splitting into two separate stack components with the outputs of 1 feeding into the inputs of the other. Same pattern used when provisioning a k8s cluster in 1 component and configuring it in the next. I also described using a provider declaration inside a module here [2] with its caveats.
I would probably leverage workspaces here, each workspace has a single subscription plus the management one. I have been able to use "workarounds" for many of these issues but one that to this day affects me is the inability to upgrade the provider version in stages. If Hashicorp is planning to implement this feature, please consider a provision to allow to have different provider versions even if that means there is a single version for the entire workspace, in other words, allow variables in the version string. [1] https://github.com/DavidGamba/dgtools/blob/master/bt/README.adoc |
Really an edge (no pun intended) case : If you use AWS Lambda@Edge, and want to configure the log groups in cloud watch, you need to do it for each region where the lambda might run (aws-lambda#265) . In such case, it would be great to loop over a list of regions and just populate all the log groups. In addition comes the issue of not being able to create providers in sub module files. Thus, we have to populate a list of providers in the parent module and then pass them all to the submodule and still have to repeat ourselves for each provided provider variation ! |
Hi @apparentlymart,
Based on your at least a year message from 2020 and that v1.0 release is complete, is there any change this feature has worked it's way up the priority list yet? |
This feature is blocked by there being no known design with which to implement it, not by prioritization. We have attempted to solve it many times since before this issue was even open. There are significant design challenges to supporting both this and the related request of declaring provider configuration instances dynamically using something like Although it's not the only problem, the most significant problem to be solved is what must happen when something exists in the remote system but no longer exists in the configuration. Terraform needs a valid and functioning provider configuration to plan and apply the "destroy" for those objects, and even in today's Terraform people often get themselves "trapped" by removing even a statically-declared-and-assigned provider configuration at the same time as removing the resources that belong to it. Every time we've tried a prototype of making it more dynamic that problem arises almost immediately, because being able to dynamically remove provider instances and resource instances together is an unspoken assumption in this feature request. The new language for Terraform Stacks will include a fresh attempt at solving this that benefits from the fact that the Stacks language is a bit of a fresh start not so constrained by existing Terraform behavior, but even there we don't yet have a completely satisfactory solution to the above problem. It is an area of ongoing research. If the design for Stacks is ultimately successful then some form of it might make it into the traditional Terraform language, but with what we know right now it seems likely to require at least some small breaking changes to the language to make it work. |
I would suggest for consideration a slightly weakened set of requirements:
As long as the user has reasonable confidence they aren't going to walk off a cliff (no. 2) I suspect dealing with a less-than-ideal implementation (i.e. no. 1 and no. 3) is something users would likely prefer over no implementation at all. (Heck, no. 1 is already a fact of life in some parts of TF in that the system forces the user to do thing TF's way rather than the way the user wants.) Regarding no. 3, the set of things that TF can do expands significantly if you are willing to accept "apply in a loop until you reach a fixed point" as normal practice. (I wonder what fraction of people already do that anyway, just to be sure things worked correctly?) For example, my current works-around for this missing functionality is to use a |
Why can't the removal of a provider follow one of these two behaviours?
In addition to this it'd be great to be able to configure a provider (static or in a provider "random" {
disabled {
mode = "delete"
}
} |
What if the alias mappings were stored in the state. So that if you remove a provider from the for_each , terraform can still figure out which provider was used to create the resources, as long as that provider still exists at the top level. |
I suspect that the cases that don't already work and would naturally fall under your "as long as" are rare. For one thing, I think some form of what you proposed would already be needed to support the case where someone deletes a module that uses aliases. That said, I can think of a few ways to contort configs that would normally lose the provider before the resources are deleted so that they pin it for just long enough to satisfy that "as long as" requirement. |
Well, you could temporarily keep the provider definition, but remove its usage from the module. |
In addition to my previous comment some of this pain was introduced/increased by the decision to artificially separate the provider architecture from the reality of how identities can be used. For example an AWS provider can be authenticated to access multiple regions but the provider artificially constrains this to a single region and requires multiple provider instances to be created. So in addition to being able to use locals {
aws_provider_regions = [
"us-east-1"
"eu-west-1"
]
}
provider "aws" {
override = [ for region in local.aws_provider_regions : { alias = region, region = region}]
}
resource "aws_s3_bucket" "example" {
for_each = toset(local.aws_provider_regions)
provider = aws.default[each.value]
bucket = "my-tf-test-bucket-{each.value}"
} As this is still a single provider configuration, which should be tied to a single identity, it shouldn't require any architectural changes as the provider removal pattern doesn't change (the overrides would need to be stored in state to support removal). All it's intended to do is to support replacing a finite number of provider values which can override values used in each resource call. This could also be implemented to work without the upcoming changes to support know values in the provider config by exposing the override aliases on the provider (e.g. |
I remember that the AWS provider team was at one point investigating the possibility of changing to a posture similar to some other providers where the provider configuration's I don't know what the outcome of that was, but I expect it would be a huge effort to retrofit every resource type in that provider and so unfortunately while I agree that it would have been better for the provider to have been designed that way in the first place -- and for the general rule to be that provider configurations focus on caller identity rather than object location -- we probably need to find a way to make this work with the provider we have. Of course, we also don't yet know how to retrofit this idea into Terraform Core. We are using the relatively-more-greenfield new language for the forthcoming Terraform Stacks as a test bed for making this work -- in Stacks each instance of a component can have a separate provider configuration, which is the same idea as this issue but at a coarser level of abstraction. If that design proves successful then some form of it can probably be brought into the traditional Terraform language too, but it seems likely to require some technically-breaking changes to the language, so we need to make sure the design seems right in a less-constrained setting before embarking on that. |
@apparentlymart my suggestion above wouldn't require any resource level changes and only minimal language changes for configuring and using the overridden providers; these should be non-breaking if not used and should require explicit opt in on a per-provider basis. Terraform would then be responsible for duplicating providers based of the default provider configuration and the overrides; but once the providers were configured everything would work as if the providers were explicitly defined. |
Hi @stevehipwell, The unanswered questions here are not really about syntax but rather about how Terraform should react to that syntax and how to introduce it without breaking existing working modules. There are various assumptions in Terraform's current design and implementation that make this difficult, including but not limited to:
The most recent idea in comments above seems to hit all three of these challenges, and so it is not a sufficient solution. I'm pretty sure I've already made variations of these points in various comments upthread here and in the related issues, but I'm restating this here just so that it's all aggregated together at the (current) end of the discussion. While I do appreciate the enthusiasm in proposing potential solutions, I'd ask participants to recognize that if this were a straightforward problem it would likely have been solved already, and that it remains unsolved because various previous attempts to solve it have been incomplete or unsatisfactory. As I mentioned in my previous comment, we are using the relatively clean slate of the new stacks language to cancel out some of the existing constraints so that we can apply some of the earlier design ideas. Of course that doesn't free us from having to still navigate the same constraints in the traditional Terraform language, but since it seems like the only path forward is making a breaking change we'd prefer to get experience with the new design in a less risky setting first and then, if successful, figure out what is the smallest breaking change we could make to get there and what strategy we ought to follow to navigate that breaking change without splitting the ecosystem. It's highly unlikely that any single comment on this issue will be sufficient to allow this issue to proceed immediately. |
There is an open PR to allow soecifying the region per resource. But it hasn't gotten a lot of activity recently. |
@apparentlymart thanks for replying, I know there is a lot of nuance I'm missing as I'm only looking at this from an external position. For context I have a requirement to configure a unknown number of different AWS accounts from a single Terraform workspace using a single IAM identity, the imperative (bash) version of this is trivial but the declarative (Terraform) version of this is impossible. I think supporting a better provider removal pattern (I suggested one here) would be beneficial in all cases, but I'm not sure it's a prerequisite for a pattern that overrides a single provider. The point of configuring a single provider with overrides is that at the abstract architectural level you've added capabilities without requiring new components. I understand that there are language limitations, and thank you for clarifying some of them above, but conceptually this pattern would be a generic version of the AWS PR that @tmccombs linked above but without requiring each supporting resource to be modified. Would this updated language pattern be a better fit? The resource locals {
aws_provider_regions = [
"us-east-1"
"eu-west-1"
]
}
provider "aws" {
overrides = { for region in local.aws_provider_regions : region => { region = region } }
}
resource "aws_s3_bucket" "example" {
for_each = toset(local.aws_provider_regions)
provider_override = each.value
bucket = "my-tf-test-bucket-{each.value}"
} I think the actual implementation here could follow one of two patterns:
Option 1 looks to be the least impactful, assuming it is possible to pre-process the Terraform config, but option 2 would probably be a better long term pattern. I think the thing here is that they're not mutually exclusive, if the language support was added then all providers could easily opt in to option 1 (specify overridable config fields) while some providers could take further steps to use option 2 if it was implemented by Terraform at a future point in time. |
Per the assumption you provided:
I don't think that is accurate. Most solutions provided have recognized the need to statically declare providers in advance of using them to implement a resource or module. The underlying problem being discussed is how to dynamically reference an individual provider from a static set of them when implementing a resource, and the syntax currently does not provide for doing so. |
I beg to differ. In the case where I'd want to dynamically select a provider to pass to a module In the case promoting my interest in this issue, that is exactly what I do, just via the back door of manually expanding the "
|
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
If you don't mind it being a total hack-around you can do a self generating TF config. In your case with two configs, you could also have the first config generate part of the second config which would remove some of the bootstrapping strangeness, though the viability of making that work would depend on how you store and locate the configs from each other. |
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as duplicate.
Their official answer will probably be to use terraform stacks? |
Circular referencing this issue to a "work around" article referencing this issue |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
Has anyone discovered a genuine workaround for generating modules with different providers through scripting, or is that still the primary approach? I'd love to hear any innovative solutions or ideas! |
Use-cases
I'd like to be able to provision the same set of resources in multiple regions a
for_each
on a module. However, looping over providers (which are tied to regions) is currently not supported.We deploy most of our infra in 2 regions in an active-passive configuration. So being able to instantiate both regions using the same module block would be a huuuge win. It's also our primary use case for for_each on modules being implemented in #17519.
Proposal
Proposed syntax from @jakebiesinger-onduo
Another option would be to de-couple the region from providers, and allow the region to be passed in to individual resources that are region aware. As far as I know, both AWS and GCP credentials at least are global.
References
The text was updated successfully, but these errors were encountered: