Consume provider configuration from a submodule

I’m just wondering if the following is technically feasible, to consume a declared provider block FROM a submodule ? This way, we could define a default provider (with role_arn as a variable) and all possible provider aliases in one place, and call that submodule, instead of symlinking a static file around…:

Contents of main.tf in submodule: (environment)

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "~> 3.37"
    }
  }
}

provider "aws" {
  region = var.region
  assume_role {
role_arn     = var.role_arn
session_name = local.environment.session_name
external_id  = local.environment.external_id
  }
}

provider "aws" {
  region = local.environment.region
  alias  = "networking"
  assume_role {
    role_arn     = local.environment.aws_accounts.networking.role_arn
    session_name = local.environment.session_name
    external_id  = local.environment.external_id
  }
}

Contents of main.tf in root module:

module "environment" {
  source = "../environment"
  role_arn = module.global.environment.aws_accounts.bastion.role_arn
}


resource "aws_ssm_parameter" "foo" {
  provider = aws.networking
  name  = "/test"
  type  = "String"
  value = "test"
}

resource "aws_ssm_parameter" "foo" {
  name  = "/test"
  type  = "String"
  value = "test"
}

Hi @edwdev,

Provider configurations only inherit downwards, because modern Terraform largely expects provider configurations to only be in the root module. (Provider configurations elsewhere remain supported in some cases for backward-compatibility but are not recommended.)

One way to get a similar effect to what you described here could be to have the shared module export the relevant settings as output values and then have the root module just assign them in to the relevant provider blocks:

output "main_aws_configuration" {
  value = {
    region = var.region
    assume_role = {
      role_arn     = var.role_arn
      session_name = local.environment.session_name
      external_id  = local.environment.external_id
    }
  }
}

output "networking_aws_configuration" {
  value = {
    region = local.environment.region
    assume_role = {
      role_arn     = local.environment.aws_accounts.networking.role_arn
      session_name = local.environment.session_name
      external_id  = local.environment.external_id
    }
  }
}

Then in the calling module:

module "environment" {
  source = "../environment"

  role_arn = module.global.environment.aws_accounts.bastion.role_arn
  region   = var.region
}

provider "aws" {
  region = module.environment.main_aws_configuration.region
  assume_role {
    role_arn     = module.environment.main_aws_configuration.assume_role.role_arn
    session_name = module.environment.main_aws_configuration.assume_role.session_name
    external_id  = module.environment.main_aws_configuration.assume_role.external_id
  }
}

provider "aws" {
  alias = "networking"

  region = module.environment.networking_aws_configuration.region
  assume_role {
    role_arn     = module.environment.networking_aws_configuration.assume_role.role_arn
    session_name = module.environment.networking_aws_configuration.assume_role.session_name
    external_id  = module.environment.networking_aws_configuration.assume_role.external_id
  }
}

This way your methodology for choosing those values is factored out into the shared module but the provider configurations still belong to the root module, where they can then pass to or be inherited by any other child modules.

One trick to be careful of here is that the environment module might also need to interact with the AWS provider, in which case you’d need to avoid a chicken-and-egg problem here by giving it access to a minimal “bootstrapping” provider configuration that is independent from the ones it’ll be used to configure:

provider "aws" {
  alias = "bootstrap"

  region = var.region
}

module "environment" {
  source = "../environment"

  role_arn = module.global.environment.aws_accounts.bastion.role_arn
  region   = var.region

  providers = {
    aws = aws.bootstrap
  }
}

If you can write your environment module so that it doesn’t make use of the AWS provider then that’d be even better, of course. If that’s the case then you can force that issue by intentionally passing the module no provider configurations, so that future changes to it can’t inadvertently try to use one of the provider configurations it’s being used to configure:

module "environment" {
  source = "../environment"

  role_arn = module.global.environment.aws_accounts.bastion.role_arn
  region   = var.region

  # overrides the default behavior of
  # inheriting the default AWS provider
  # configuration.
  providers = {}
}

Hi @apparentlymart,

I am trying something similar in my providers file from my root module and would like to confirm whether that is indeed possible.

provider “azurerm” {
subscription_id = module.deploy_subscription.subscription_id
alias = “new-subs”
features {}
}

The Terraform documentation seems to imply that this is not possible or maybe my understanding is not correct:

" You can use expressions in the values of these configuration arguments, but can only reference values that are known before the configuration is applied. This means you can safely reference input variables, but not attributes exported by resources (with an exception for resource arguments that are specified directly in the configuration)."

Thanks in advance!