Working with multiple providers and nested modules/resources

Hi,

In my resource block, I have declared the provider attribute and assigned a variable:

resource "azurerm_subscription" "this" {
  provider          = var.provider_alias
  subscription_name = var.subscription_name
  billing_scope_id  = data.azurerm_billing_enrollment_account_scope.this.id
  workload          = var.subscription_workload
  tags              = var.provider_alias
}

VSCode extension is complaining about var.provider_alias

No declaration found for "var.provider_alias
The variable is declared in the variables.tf file.

I haven’t worked with Terraform for a while, but back in the day (0.13), I recall a “thing” about not being able to use variables for some of the attributes in the provider block.

If this is the case, I’ll have to declare the entire provider block for each resource and each provider alias :frowning: and that’s a lot.

If I’ve got it wrong or it’s no longer a “thing”, I’d be grateful if someone could point me in the right direction.

Thank you in advance.

Hi @woter324,

In Terraform provider configurations are a special kind of object that, unlike all other kinds of object in Terraform, can automatically propagate between module boundaries so that things are simpler in the common case where there’s only one configuration for each provider.

However, that does mean that when things get more complicated – like in your case, where you have multiple provider configurations – the syntax for talking about provider configurations is quite different than the syntax for passing “normal” values between modules.

I’m making some guesses because you’ve only shared a tiny fragment of your configuration, but my assumption is that you’ve written a shared module which is designed to work with a single provider but then you’ve called it multiple times from your root module and want to pass a different provider configuration to each instance.

It is possible to write a configuration structured in that way. There are two parts to it: writing the shared module, and then writing the calls to that module.

Writing the shared module

If each instance of your shared module only works with one provider configuration at a time then this part is relatively “normal”: you can just write it to assume that a default (unaliased) configuration of hashicorp/azurerm will be provided by the caller of the module.

That means that you’d omit any provider arguments inside all of your resource blocks. As far as this module is concerned, you’re using the default (unaliased) configuration, which is the default behavior.

Writing the calling module

The module which calls the shared module is the one that must deal with the additional complexity of there being multiple configurations of the same provider.

Assuming that the module is being called from the root module, the root module has two responsibilities:

  1. To declare each of the multiple aliased provider configurations.
  2. To pass the appropriate aliased configuration as the default configuration for each instance of the module.

Here’s a concrete example of that:

provider "azurerm" {
  alias = "a"

  # ...
}

provider "azurerm" {
  alias = "b"

  # ...
}

module "example_a" {
  # This should be the "shared module" discussed
  # in the previous section.
  source = "../modules/example"
  providers = {
    azurerm = azurerm.a
  }

  # (any normal variable definitions here)

}

module "example_b" {
  # This should be the "shared module" discussed
  # in the previous section.
  source = "../modules/example"
  providers = {
    azurerm = azurerm.b
  }

  # (any normal variable definitions here)

}

The above declares that the root module has two non-default (aliased) configurations for the provider, named azurerm.a and azurerm.b.

It then makes two calls to the shared module discussed in the previous section. That module expects to be provided a default (unaliased) configuration for the provider, so you’d use the providers argument to explain to Terraform how to populate the child module’s namespace of provider configurations based on what’s available in the calling module.

The providers argument in a module block is the same idea as the provider argument in a resource block, but it chooses the entire table of provider configurations available to the child module rather than just a single provider.

  providers = {
    azurerm = azurerm.a
  }

The above example means “the default configuration for azurerm in the child module is the same as the azurerm.a configuration in this module”. The child module can then just use the default provider assignment behavior without any special additional arguments.

Thank you @apparentlymart for your very detailed reply.

The way I “thought” I had to use multiple providers and the way they actually work are totally different. With your help, I’ve got it working.

Thanks again.