Skip to content
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

Dynamic provider configuration assignment #300

Open
RafPe opened this issue Aug 31, 2023 · 48 comments · May be fixed by #2186
Open

Dynamic provider configuration assignment #300

RafPe opened this issue Aug 31, 2023 · 48 comments · May be fixed by #2186
Assignees
Labels
accepted This issue has been accepted for implementation. enhancement New feature or request
Milestone

Comments

@RafPe
Copy link

RafPe commented Aug 31, 2023

Suggest an existing issue in legacy Terraform to fix in OpenTF.

To increase automation we struggle many times when configuring providers. It would be great to finally be able to configure them dynamically or at least to be able to dynamically reference them instead of having all of that statically typed


Implementation PR: #2054

@RafPe RafPe changed the title Dynamic provider configuration assignment Open existing Terraform Issue [25244]: [Dynamic provider configuration assignment] Aug 31, 2023
@roni-frantchi roni-frantchi transferred this issue from opentofu/roadmap Sep 6, 2023
@roni-frantchi roni-frantchi changed the title Open existing Terraform Issue [25244]: [Dynamic provider configuration assignment] Dynamic provider configuration assignment Sep 6, 2023
@roni-frantchi
Copy link
Collaborator

Hey @RafPe thanks for submitting!

To help us maintain a clear separation between opentf and hashicorp's offerings, we're asking that people describe issues that are in other repositories rather than linking those directly.
I've thus scrubbed out any links to said issues/PRs.

If there's any more context or description to the problem you think would be good to share and add in please do.

Thanks!

@RafPe
Copy link
Author

RafPe commented Sep 7, 2023

@roni-frantchi Sure - I understand. Let me explain more in detail what I mean here.

Situation now :

we define our providers more less in a static way ( there is a level where we can use variables ) and then reference them completely static (no vars, no dynamics etc )

provider "aws" {
  region = "eu-central-1"
  alias  = "x1"
}

provider "aws" {
  region = "eu-west-1"
  alias  = "x2"
}



resource "aws_resource" "x1" {
  provider = aws.x1
}

resource "aws_resource" "x2" {
  provider = aws.x2
}

Scenario 1

dynamically referencing providers: Having the above providers, we could either have something in form of singleton object called providers to which one we could reference via alias the pointer to the provider we defined.

provider "aws" {
  region = "eu-central-1"
  alias  = "x1"
}

provider "aws" {
  region = "eu-west-1"
  alias  = "x2"
}

resource "aws_resource" "x1" {
  provider = providers.aws["x1"]
}

resource "aws_resource" "x2" {
  provider = providers.aws["x2"]
}

Scenario 2

dynamically referencing providers with for_each/count: Having the same above providers, we could either have something in form of singleton object called providers to which one we could reference via alias the pointer to the provider we defined but also support for_each/count on the providers level to create them dynamically

locals {
  aws_accounts = [
    { "aws_account_id": "123456789012", "foo_value": "foo",    "bar_value": "bar"    },
    { "aws_account_id": "987654321098", "foo_value": "foofoo", "bar_value": "barbar" },
    ]
}

## Here's the proposed magic... `provider.for_each`
provider "aws" {
  for_each = local.aws_accounts
  alias = each.value.aws_account_id

  assume_role {
    role_arn    = "arn:aws:iam::${each.value.aws_account_id}:role/TerraformAccessRole"
  }
}

## Here is the magic of referencing them in our resources 

resource "aws_resource" "x1" {
  for_each = local.aws_accounts

  provider = providers.aws[each.value.aws_account_id]
}

I believe this approach ( especially in bigger organisations ) would allow for more automation and maintaining the logic within the tool instead of leveraging external templating mechanism

Please let me know if the above makes sense :)

@roni-frantchi roni-frantchi added the enhancement New feature or request label Sep 13, 2023
@cube2222 cube2222 added the pending-decision This issue has not been accepted for implementation nor rejected. It's still open to discussion. label Sep 14, 2023
@jwenz723
Copy link

In my situation I have multiple AWS accounts (dev, test, prod). All accounts use the us-west-2 region, but the prod account also uses a few other regions.

So in my case I really need one provider enabled when deploying to dev and test and multiple providers enabled when deploying to prod. I am able to work around this currently by just including all regional providers in all accounts and I just don’t use the extra providers during a dev or test deploy.

This work around no longer works though when you have a module that needs to be deployed to multiple AWS partitions. For example standard and govcloud. A govcloud provider will not work when deploying to a standard partition and a standard provider will not work when deploying to govcloud. In this situation I am able to work around this by using some custom code outside of opentofu to generate the provider config before running opentofu.

It would be nice to just have the ability to do loops and conditionals on providers.

@timothyjlaurent
Copy link

This could be OpenTF's killer app ;)

@RafPe
Copy link
Author

RafPe commented Sep 30, 2023

@roni-frantchi as I myself would be open to contributing to this ... would eb anyone else experienced already in the code base that could guide me towards making this possible ?

@roni-frantchi
Copy link
Collaborator

Hey @RafPe !

I myself would be open to contributing to this would eb anyone else experienced already in the code base that could guide me towards making this possible ?

Very much appreciated.
It'd be harder for us to provide such guidance, as right now our core team focuses on:

  1. Rolling out an alpha in the next couple of days
  2. Focusing on a design for a registry to power the stable release

Of course if you're willing to dive in there yourself or assisted by anyone else from the community would love to see such a design from you

@murarisumit
Copy link

murarisumit commented Oct 24, 2023

Ref to issue in terraform repo, quite a bit of discussion over there on it: hashicorp/terraform#19932

@dbhaigh
Copy link

dbhaigh commented Nov 3, 2023

Issue 19932 has been open since Jan 7th 2019 - it doesn't look like it's been given much love since

Plenty of people wanting this ability (myself included)

This ability would be fantastic, and yes, bring people over to the Open Source side

@Chandra2614
Copy link

This needs to be fixed ASAP, 3 years are a lot

@cam72cam cam72cam removed the legacy label Jan 18, 2024
@cube2222 cube2222 added the needs-rfc This issue needs an RFC prior to being accepted or, if it's accepted, prior to being implemented. label Jan 18, 2024
@cube2222
Copy link
Contributor

With the way providers currently work, esp. with for_each blocks, this would require a very technical and detailed RFC prior to being accepted.

But overall, we're open to adding this as an OpenTofu feature, if we come up with a good way to do so.

@RafPe
Copy link
Author

RafPe commented Jan 18, 2024

@cube2222 maybe I do not see the whole technical picture - but would it for example be possible to split it in two working items ?

  1. to make it accessible as just static array so we can index it into via a key
  2. Think of making it super dynamic with variable interpolations

Feels like doing the 1 would be less of an impact from a major change as you hihlighted dependencies on that

@cam72cam
Copy link
Member

I think that makes sense, as #2 would likely relate to #1042

@ImIOImI
Copy link

ImIOImI commented Jan 19, 2024

Scenario 2 feels like it'll cover most of the cases I encounter in the wild... but I say without any hesitation that no matter how unergonomic the solution is, I'll figure it out and use it and be happy to have it.

That being said, would it be possible to configure and pass a provider like:

locals {
  my_object = [
    { provider: providers.aws["dev"], "foo_value": "foo",    "bar_value": "bar"    },
    { provider: providers.aws["dev"], "foo_value": "apple",    "bar_value": "pear"    },
    { provider: providers.aws["prd"], "foo_value": "foo",    "bar_value": "bar"    },
    ]
  aws_accounts = {
    dev = {
      aws_account_id": "123456789012"
     },
    prd = { 
      "aws_account_id": "987654321098"
    }
}

provider "aws" {
  for_each = local.aws_accounts
  alias = each.key

  assume_role {
    role_arn    = "arn:aws:iam::${each.value.aws_account_id}:role/TerraformAccessRole"
  }
}

resource "aws_resource" "x1" {
  for_each = local.my_object

  provider = each.value.provider
}

@cam72cam
Copy link
Member

Something worth noting, I think the provider. prefix is probably required as part of this work (at least for expressions). Otherwise your provider names are top level identifiers and can conflict with other things like "local"

@nitrocode
Copy link

In this case this helps for people in the mean time.

I've used workspaces with a specific tfvars containing a variable for the region as a workaround for creating the same resources in multiple regions or multiple accounts.

main.tf

variable region {}
variable account {}

provider aws {
  region = var.region

  assume_role {
    role_arn = "arn:aws:iam::${module.account_map.accounts[var.account]}:role/cicd"
  }
}

uw2-dev.tfvars

region = "us-west-2"
account = "dev"

Commands

terraform workspace select uw2-dev
terraform init -var-file uw2-dev.tfvars

@ImIOImI
Copy link

ImIOImI commented Feb 13, 2024

I've used workspaces with a specific tfvars containing a variable for the region as a workaround for creating the same resources in multiple regions or multiple accounts.
...

As of now, I think you're right about workspaces being the best answer when needing dynamic providers. I've done something similar like the following in Azure:

locals {
  cntxts = {
    defaults = {
      subscription_id         = "00000000-0000-0000-0000-000000000000"
      dynamic_subscription_id = "12345678-0000-0000-0000-000000000000"
      tenant_id               = "00000000-0000-0000-0000-000000000000"
    }
    dev = {
      dynamic_subscription_id = "abcdefgh-0000-0000-0000-000000000000"
    }
    infra = {
      dynamic_subscription_id = "abc123hi-0000-0000-0000-000000000000"
    }
    stg = {
      dynamic_subscription_id = "foobar12-0000-0000-0000-000000000000"
    }
    prd = {
      dynamic_subscription_id = "RunningO-utOf-Gene-ricG-UIDideas1234"
    }
  }

  contexts                = module.contexts.merged
  default_subscription_id = local.contexts.subscription_id
  dynamic_subscription_id = local.contexts.dynamic_subscription_id
  tenant_id               = local.contexts.tenant_id

  # where workspaces are like (dev, infra, stg, prd)
  context = terraform.workspace
}

module "contexts" {
  source = "Invicton-Labs/deepmerge/null"
  maps   = [
    local.cntxts.defaults,
    local.cntxts[local.context],
  ]
}

provider "azurerm" {
  alias = "default"

  subscription_id = local.default_subscription_id
  tenant_id       = local.tenant_id
}

provider "azurerm" {
  alias = "dynamic"

  subscription_id = local.dynamic_subscription_id
  tenant_id       = local.tenant_id
}

However, this often forces you into architecture patterns that could be more simply solved with a loop of providers

@vierkean
Copy link

Hello,
since HashiCorp still supported this function in terraform version 0.11 and no longer does since 0.12, I was looking for an alternative to terraform and came across tofu.
Unfortunately, the current version of tofu does not support it either.
I hope tofu will support it soon and I don't have to rebuild the organization in terraform to use a current version.

provider "aws" { alias = "${var.AccountName}" region = "${var.region}" assume_role { role_arn = "arn:aws:iam::${aws_organizations_account.account.id}:role/someRole" } }

Kind Regards

@cam72cam cam72cam removed the needs-rfc This issue needs an RFC prior to being accepted or, if it's accepted, prior to being implemented. label Jun 24, 2024
cam72cam pushed a commit that referenced this issue Oct 25, 2024
Signed-off-by: ollevche <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>
cam72cam pushed a commit that referenced this issue Oct 25, 2024
Signed-off-by: ollevche <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>
cam72cam added a commit that referenced this issue Oct 25, 2024
Signed-off-by: ollevche <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>
Co-authored-by: Christian Mesh <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>
cam72cam pushed a commit that referenced this issue Oct 25, 2024
…es (#1963)

Signed-off-by: ollevche <[email protected]>
Signed-off-by: Ronny Orot <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>
Co-authored-by: ollevche <[email protected]>
Co-authored-by: Christian Mesh <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>

Signed-off-by: Christian Mesh <[email protected]>
apparentlymart added a commit that referenced this issue Oct 28, 2024
This RFC describes an alternative approach for issue #300 that treats it
as a dynamic evaluation problem rather than as an early-eval problem.

For the moment my goal of this proposal is not to actually replace the
existing early eval proposal, but rather to draw attention to some aspects
of it that would be difficult to translate to fully-dynamic eval if we
heard requests for that later.

This document therefore aims to ask the question of firstly whether we'd
like to impose some similar constraints on the early eval approach even
though they aren't strictly needed there, and secondly whether this
realization that we don't have any current experience retroactively
changing a feature from early eval to dynamic eval represents enough risk
that we might prefer to put even the early-eval approach on pause while
we analyze the potential risks in more detail.

It's likely that if we _did_ choose to implement the dynamic approach we'd
want to refine this RFC further to draw out some more detail in the
implementation section. I've intentionally skimped on that section here
because the priority is to have the conversation about the potential new
user-facing constraints, and not to actually implement what this proposal
describes in the near future. The implementation details are included as
a starting point, but are insufficiently detailed to actually begin
implementation.

Signed-Off-By: Martin Atkins <[email protected]>
cam72cam added a commit that referenced this issue Nov 5, 2024
Signed-off-by: ollevche <[email protected]>
Co-authored-by: Christian Mesh <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>
cam72cam pushed a commit that referenced this issue Nov 5, 2024
Signed-off-by: ollevche <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>
cam72cam pushed a commit that referenced this issue Nov 5, 2024
Signed-off-by: ollevche <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>
cam72cam added a commit that referenced this issue Nov 5, 2024
Signed-off-by: ollevche <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>
Co-authored-by: Christian Mesh <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>
cam72cam pushed a commit that referenced this issue Nov 5, 2024
…es (#1963)

Signed-off-by: ollevche <[email protected]>
Signed-off-by: Ronny Orot <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>
Co-authored-by: ollevche <[email protected]>
Co-authored-by: Christian Mesh <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>

Signed-off-by: Christian Mesh <[email protected]>
cam72cam added a commit that referenced this issue Nov 5, 2024
Signed-off-by: ollevche <[email protected]>
Co-authored-by: Christian Mesh <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>
cam72cam pushed a commit that referenced this issue Nov 5, 2024
Signed-off-by: ollevche <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>
cam72cam pushed a commit that referenced this issue Nov 5, 2024
Signed-off-by: ollevche <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>
cam72cam added a commit that referenced this issue Nov 5, 2024
Signed-off-by: ollevche <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>
Co-authored-by: Christian Mesh <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>
cam72cam pushed a commit that referenced this issue Nov 5, 2024
…es (#1963)

Signed-off-by: ollevche <[email protected]>
Signed-off-by: Ronny Orot <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>
Co-authored-by: ollevche <[email protected]>
Co-authored-by: Christian Mesh <[email protected]>
Signed-off-by: Christian Mesh <[email protected]>

Signed-off-by: Christian Mesh <[email protected]>
@vitaly-dt
Copy link

Will this feature allow us to use the provider block as a dependency?

@thuck
Copy link

thuck commented Nov 7, 2024

I was testing the v1.9.0-alpha1 and one feedback is that the error message should include the key somewhere.
I was testing the datadog provider, and one of my keys was wrong, the error message is:

│ Error: 403 Forbidden
│ 
│   with provider["registry.opentofu.org/datadog/datadog"].bl,
│   on main.tf line 36, in provider "datadog":
│   36: provider "datadog" {

As the key is not part of the error, it's very hard to find which one has an issue; it would be nice to have something more like:

│ Error: 403 Forbidden
│ 
│   with provider["registry.opentofu.org/datadog/datadog"].bl["KEY"],
│   on main.tf line 36, in provider "datadog":
│   36: provider "datadog" {

@cam72cam
Copy link
Member

cam72cam commented Nov 7, 2024

@vitaly-dt

Will this feature allow us to use the provider block as a dependency?

Not in this feature. Take a look at #2088 for thoughts to expand upon the existing provider configuration.

@cam72cam
Copy link
Member

cam72cam commented Nov 7, 2024

@thuck thanks for the feedback, can you post the full error so I can make sure I update that log message?

@thuck
Copy link

thuck commented Nov 7, 2024

@cam72cam no problem at all.

tofu plan

Planning failed. OpenTofu encountered an error while generating this plan.

╷
│ Error: 403 Forbidden
│ 
│   with provider["registry.opentofu.org/datadog/datadog"].bl,
│   on main.tf line 36, in provider "datadog":
│   36: provider "datadog" {
│ 
╵
╷
│ Error: 403 Forbidden
│ 
│   with provider["registry.opentofu.org/datadog/datadog"].bl,
│   on main.tf line 36, in provider "datadog":
│   36: provider "datadog" {
│ 
╵
╷
│ Error: 403 Forbidden
│ 
│   with provider["registry.opentofu.org/datadog/datadog"].bl,
│   on main.tf line 36, in provider "datadog":
│   36: provider "datadog" {
│ 
╵
╷
│ Error: 403 Forbidden
│ 
│   with provider["registry.opentofu.org/datadog/datadog"].bl,
│   on main.tf line 36, in provider "datadog":
│   36: provider "datadog" {
│ 
╵
╷
│ Error: 403 Forbidden
│ 
│   with provider["registry.opentofu.org/datadog/datadog"].bl,
│   on main.tf line 36, in provider "datadog":
│   36: provider "datadog" {
│ 
╵
╷
│ Error: 403 Forbidden
│ 
│   with provider["registry.opentofu.org/datadog/datadog"].bl,
│   on main.tf line 36, in provider "datadog":
│   36: provider "datadog" {
│ 
╵

Here the main.tf that I'm using to test:

terraform {
  required_providers {
    datadog = {
      source = "DataDog/datadog"
    }
  }
}

locals {
  datadog = jsondecode(file("dd_keys.json"))
}

provider "datadog" {
  for_each = local.datadog
  alias    = "bl"
  api_url  = each.value.api_url
  api_key  = each.value.api_key
  app_key  = each.value.app_key

}

data "datadog_roles" "foo" {
  for_each = local.datadog
  provider = datadog.bl[each.key]
}

@apparentlymart
Copy link
Contributor

apparentlymart commented Nov 7, 2024

Hi all!

I realize that yesterday I neglected to explicitly mention here that v1.9.0-alpha2 includes an early version of a solution for this feature that we've been working on over in #2123.

If you have the time and interest, we'd love to hear feedback from folks who try this with somewhat-realistic configurations that match whatever problems had caused them to be interested in this issue. Please don't use this in production, though!

It's tough to keep track of multiple threads of conversation in a single GitHub issue, and we'd like to keep this issue focused on the use-case rather than on details of the initial solution to it, so if you do try the alpha and have feedback please open a new issue for each feedback item, and then we'll collect links to them in #2123 to make sure we can investigate each one.

Thanks!

EDIT: I originally wrote this comment linking to alpha1, but we've just published alpha2 with some important fixes so I'd ask that anyone who wants to test this should focus on testing with alpha2 instead.

@kylan11
Copy link

kylan11 commented Nov 8, 2024

I'm attempting to fetch a list of AWS Account IDs from the remote state of a separate OpenTofu configuration, and dynamically generate providers for each on 1.9.0-alpha2.

Is this outside the scope of the current enhancement?

data "terraform_remote_state" "org" {
  backend = "s3"
  config = {
    bucket = var.s3_bucket_name
    key    = "terraform-aws-org/terraform.tfstate"
    region = var.aws_region
  }
}

locals {
  account_ids = values(data.terraform_remote_state.org.outputs.member_account_ids)
}

provider "aws" {
  alias = "child"

  for_each = toset(local.account_ids)

  region = var.aws_region
  assume_role {
    role_arn     = "arn:aws:iam::${each.key}:role/OrganizationAccountAccessRole"
    session_name = "TerraformSession-${each.key}"
  }
}
tofu plan
╷
│ Error: Dynamic value in static context
│ 
│   on locals.tf line 4, in locals:4:   account_ids = values(data.terraform_remote_state.org.outputs.member_account_ids)
│ 
│ Unable to use data.terraform_remote_state.org in static context, which is required by local.account_ids
╵
╷
│ Error: Unable to compute static value
│ 
│   on providers.tf line 5, in provider "aws":5:   for_each = toset(local.account_ids)
│ 
│ provider.aws.child.for_each depends on local.account_ids which is not available
╵

@apparentlymart
Copy link
Contributor

Hi @kylan11,

Indeed, for this first iteration of the feature the for_each argument in provider blocks uses the same "early evaluation" mechanism that OpenTofu uses for expressions in backend blocks and module source strings. So the error you encountered is expected for the current version.

We are hoping to extend it to be fully-dynamic in future -- #2088 is representing that -- but we wanted to get an initial version of this out sooner, with less risk, and then iterate on it.

We'll be making new issues to represent some of the intentionally-omitted capabilities in this first release soon, as we get closer to the v1.9.0 beta period and its final release. I expect we'll add a comment linking to all of those issues here once we've created them, so that folks can vote on them separately to help prioritize that work.

For now you will need to specify your set of account ids either statically inside the configuration, or as an input variable.

Thanks!

@kylan11
Copy link

kylan11 commented Nov 8, 2024

@apparentlymart Apologies for posting here! I read your last comment too late.
I've opened an issue for this use case.

Thank you!

@cam72cam cam72cam linked a pull request Nov 19, 2024 that will close this issue
10 tasks
@hegerdes
Copy link

hegerdes commented Dec 1, 2024

This is really great and interesting, thanks for working on it 👍

I have a question though. Are the following use cases covered by this issue?

Usecase1:
I have a opentofu/terraform configuration that creates a Kubernetes cluster (EKS/AKS/GKE or any other) and I want to apply an helm chart or manifest immediately after the creation. Currently the plan fails because the kubeconfig does not exist yet or the cluster DNS endpoint does not answer yet since it does not yet exist. There are workaround with running tofu/tf twice or splitting state but it all feels a little hacky.

Usecase2:
I am a module developer and want to offer my comunity a module to set DNS on all our DNS zone hosten by different entities. They could provide an input like this:

variable "dns_record" {
  type = object({
    create   = bool
    zone     = string
    provider = string
    record   = string
  })
  description = "DNS record for the controlplane. Provider can be cloudflare, aws, azure"
}

My provider config has all needed providers. Currently this fails as I must provide valid credentials for all providers even if only Cloudflare is actually used. Only the providers which have aktive resources should be initialized in the first place.

Question:
I would summarize these as late provider initialization. Is this covered by this issue or out of scope and would require a extra issue?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accepted This issue has been accepted for implementation. enhancement New feature or request
Projects
None yet