2
votes

so below is my project file structure:

├── main.tf
├── tunnel
│   ├── main.tf
│   └── variables.tf
└── variables.tf

I am trying to use multiple providers in Terraform 0.15.1 as described here -> https://www.terraform.io/docs/language/modules/develop/providers.html

After following the example I am not able to get it work. I have now simplified my code down to just use one single provider alias(keep it as simple as possible). The error I am getting is:

╷
│ Error: Missing required argument
│ 
│ The argument "region" is required, but was not set.
╵

My main.tf file in root directory:

module "tunnel" {
  source    = "./tunnel"
  providers = {
    aws.r = aws.requester
  }
}

my variables.tf in root directory:

provider "aws" {
  alias  = "requester"
  region = "ap-southeast-2"
  profile = "benchmark"
}

my tunnel/variables.tf file:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 2.7.0"
      configuration_aliases = [ aws.r ]
    }
  }
}

data "aws_region" "current" {}

data "aws_caller_identity" "current" {}

my tunnel/main.tf file:

# Requester's side of the connection.
resource "aws_vpc_peering_connection" "peer" {
  vpc_id        = "vpc-xxxxxxxxxxxxxxxxx"
  peer_vpc_id   = "vpc-xxxxxxxxxxxxxxxxx"
  peer_owner_id = data.aws_caller_identity.current.account_id
  peer_region   = data.aws_region.current.name
  auto_accept   = false

  tags = {
    Side = "Requester"
  }
}

I don't understand why I am getting this error? The goal with this code eventually is to automate both sides of the vpc peering as shown here -> https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_peering_connection_accepter . But I am currently stuck on getting two aws providers working with different credentials (one provider in the example above to simplify things).

When I remove alias = "requester" from the root main.tf:

provider "aws" {
//  alias  = "requester"
  region = "ap-southeast-2"
  profile = "benchmark"
}

and the provider config from main.tf in root path:

module "tunnel" {
  source    = "./tunnel"
//  providers = {
//    aws.r = aws.requester
//  }
}

and the alias config from tunnel/variables.tf:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 2.7.0"
//      configuration_aliases = [ aws.r ]
    }
  }
}

plan works fine:

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.tunnel.aws_vpc_peering_connection.peer will be created
  + resource "aws_vpc_peering_connection" "peer" {
      + accept_status = (known after apply)
      + auto_accept   = false
      + id            = (known after apply)
      + peer_owner_id = "xxxxxxx"
      + peer_region   = "ap-southeast-2"
      + peer_vpc_id   = "vpc-xxxxxxxxxxxxxxxxx"
      + tags          = {
          + "Side" = "Requester"
        }
      + vpc_id        = "vpc-xxxxxxxxxxx"

      + accepter {
          + allow_classic_link_to_remote_vpc = (known after apply)
          + allow_remote_vpc_dns_resolution  = (known after apply)
          + allow_vpc_to_remote_classic_link = (known after apply)
        }

      + requester {
          + allow_classic_link_to_remote_vpc = (known after apply)
          + allow_remote_vpc_dns_resolution  = (known after apply)
          + allow_vpc_to_remote_classic_link = (known after apply)
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.
1

1 Answers

5
votes

This error message is the result of a few automatic behaviors Terraform has to make simpler cases simpler, but which unfortunately lead to the situation being rather unclear in more complicated situations like yours.

In your child module you've declared that it expects an alternative (aliased) provider configuration to be passed in the caller, which within this module will be known as aws.r. However, the data resources you declared afterwards don't include a provider argument to specify which provider configuration they belong to, and so Terraform is choosing the default (unaliased) provider configuration.

Unfortunately, your configuration doesn't actually have a default provider configuration, because the root module is also using an alternative provider configuration aws.requester. As a result, Terraform is automatically constructing one with an empty configuration, because that's a useful behavior for simple providers like http which don't require any special configuration. But that doesn't end up working for the aws provider, because it requires region to be set.

There are at least two different ways you could change the child module to make this work. Which of these will be most appropriate will depend on how this module fits in with your broader configuration.


The first option would be to have the child module not declare aws.r at all, and just use its default provider configuration throughout:

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

data "aws_region" "current" {}

data "aws_caller_identity" "current" {}

Because your root module doesn't have a default configuration for hashicorp/aws you'll still need to be explicit in the module call that the default provider for the module is the aws.requester provider as the root module sees it:

module "tunnel" {
  source    = "./tunnel"
  providers = {
    aws = aws.requester
  }
}

This approach is a good choice for a shared module that doesn't need more than one AWS provider configuration, because the module itself can then be totally unaware of the multiple configurations in its caller, and instead just expect to be given a default configuration for hashicorp/aws to use.

However, it wouldn't work if your child module needs to have more than one configuration for hashicorp/aws too. In that case, you'll need the other option I'll describe next.


When a shared module will work with more than one provider configuration, we need to declare a configuration_aliases entry for each configuration it expects to be passed from its caller. You only showed one called r in your examples, and I don't know what "r" represents, so for the sake of examples here I'm going to call them "src" (for "source") and "dst" for ("destination") just to have some meaningful terms to hook onto for the sake of example.

We'll start with the configuration_aliases configuration:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 2.7.0"
      configuration_aliases = [ aws.src, aws.dst ]
    }
  }
}

Each of the items given in configuration_aliases declares a configuration address that must have a corresponding entry in the providers argument in any call to this module, which we'll see later.

Since this module is not expecting to use a default (unaliased) configuration, we are now required to tell Terraform for each resource block which provider configuration it belongs to. Starting with your data resources, let's assume they belong to the "source" end:

data "aws_region" "current" {
  provider = aws.src
}

data "aws_caller_identity" "current" {
  provider = aws.src
}

I suspect that in your real system the aws_vpc_peering_connection resource you showed probably logically belongs to the "source" side too, but since you didn't show any other resources I'm just going to arbitrarily assign it to aws.dst to show what that looks like:

resource "aws_vpc_peering_connection" "peer" {
  provider = aws.dst

  vpc_id        = "vpc-xxxxxxxxxxxxxxxxx"
  peer_vpc_id   = "vpc-xxxxxxxxxxxxxxxxx"
  peer_owner_id = data.aws_caller_identity.current.account_id
  peer_region   = data.aws_region.current.name
  auto_accept   = false

  tags = {
    Side = "Requester"
  }
}

Every data and resource block in that module will need to have provider set, because there is no default provider configuration to select by default in this module.

When you call the module, you'll need to tell Terraform which of the provider configurations in the caller map to the src and dst configurations in the called module:

module "tunnel" {
  source    = "./tunnel"
  providers = {
    aws.src = aws.requester
    aws.dst = aws.peer
  }
}

As I mentioned earlier, we need one entry in providers for each of the configuration_aliases declared inside the module. It might be helpful to think of these alternative provider configurations as being somewhat similar to input variables, but they have a more specialized syntax for declaration and definition because providers are so fundamental to Terraform's execution model and need to be resolved before normal expression evaluation is possible.

Here I just arbitrarily chose "peer" as the name for a presumed second configuration you have declared in your root module. It's also valid to assign a default configuration in the caller to an alternative configuration in the called module, like aws.src = aws, but that doesn't seem to apply in your situation because you don't have a default configuration in the root module either.