Search code examples
terraformterraform-provider-azure

Is provider variable possible in terraform?


Is it still not possible to use variable provider in Terraform v0.12.6 ? In *.tfvars I have list variable supplier

supplier = ["azurerm.core-prod","azurerm.core-nonprod"]

and providers defined in provider.tf:

provider "azurerm" {
  ...
  alias           = "core-prod"
}

provider "azurerm" {
  ...
  alias = "core-nonprod"

then I want to reference it in *.tf . The example below is with 'data', but the same applies to 'resource'

data "azurerm_public_ip" "pip" {
  count = "${var.count}"
   ....
   provider = "${var.supplier[count.index]}"
} 

What is a workaround to the error:

Error: Invalid provider configuration reference

...

The provider argument requires a provider type name, optionally followed by a period and then a configuration alias.

Solution

  • It is not possible to dynamically associate a resource with a provider. Similar to how in statically-typed programming languages you typically can't dynamically switch a particular symbol to refer to a different library at runtime, Terraform needs to bind resource blocks to provider configurations before expression evaluation is possible.

    What you can do is write a module that expects to receive a provider configuration from its caller and then statically select a provider configuration per instance of that module:

    provider "azurerm" {
      # ...
    
      alias = "core-prod"
    }
    
    module "provider-agnostic-example" {
      source = "./modules/provider-agnostic-example"
    
      providers = {
        # This means that the default configuration for "azurerm" in the
        # child module is the same as the "core-prod" alias configuration
        # in this parent module.
        azurerm = azurerm.core-prod
      }
    }
    

    In this case, the module itself is provider agnostic, and so it can be used in both your production and non-production settings, but any particular use of the module must specify which it is for.

    A common approach is to have a separate configuration for each environment, with shared modules representing any characteristics the environments have in common but giving the opportunity for representing any differences that might need to exist between them. In the simplest case, this might just be two configurations that consist only of a single module block and a single provider block, each with some different arguments representing the configuration for that environment, and with the shared module containing all of the resource and data blocks. In more complex systems there might be multiple modules integrated together using module composition techniques.


    Much later update: What I wrote above is still true for Terraform at the time of writing, but OpenTofu (a fork of Terraform) introduced some new capabilities in its v1.9 release that can allow for a slightly more dynamic solution.

    First, you can define a provider configuration that has multiple instances using the new for_each argument. This would combine both of the provider configurations from the original question into a single block with two instances:

    provider "azurerm" {
      alias = "core"
      for_each = {
        prod = {
          # (settings specific to prod)
        }
        nonprod = {
          # (settings specific to nonprod)
        }
      }
    
      # Define the provider settings here, using
      # each.key and each.value to refer to the
      # settings that need to be unique to each
      # instance.
    }
    

    You can then use dynamic expressions to decide which instance of this provider configuration each instance of a resource is managed by. All instances of the resource must still be associated with the same provider block for dependency-discovery purposes, but the instance key can be dynamic.

    variable "ip_addresses" {
      type = map(object({
        # Set supplier to either "prod" or "nonprod"
        # for each IP address element.
        supplier = string
    
        # (and any other settings that you need to
        # define the query for each IP address)
      }))
    }
    
    data "azurerm_public_ip" "pip" {
      for_each = var.ip_addresses
      provider = azurerm.core[each.value.supplier]
    
      # (and whatever arguments you need to select
      # the right IP address, based on any additional
      # attributes declared in the var.ip_addresses
      # element object type.)
    } 
    

    At the time of writing this (while OpenTofu v1.9 is the latest minor release), the multi-instance provider block must be declared in the same module as the resource/data/module blocks that refer to it, because there isn't yet any syntax for passing a whole multi-instance provider configuration into a child module.

    However, you can still use a structure similar to the one I originally answered above to factor out multiple resources into a separate module that has multiple instances itself where each one refers to a different instance of the provider.

    module "provider-agnostic-example" {
      source   = "./modules/provider-agnostic-example"
      for_each = var.ip_addresses
    
      providers = {
        # This means that the default configuration for "azurerm" in the
        # child module is whichever instance of azurerm.core is selected
        # by each.value.supplier.
        azurerm = azurerm.core[each.value.supplier]
      }
    }