This project is a demonstration of using terraform provider aliasing and modular resource layout to do work in multiple AWS regions within a single terraform project.
Specifically, we'll be creating a VPC in each of three regions, configuring VPC peering between those regions, and establish routes so that resources created in those regions can talk to one other.
The project structure looks like this:
.
├── main.tf
├── provider.tf
├── variable.tf
├── versions.tf
├── per-region
│ └── main.tf
├── vpc
│ └── main.tf
└── vpc-peering
└── main.tf
The top-level project's provider configuration specifies multiple aws
providers and creates an alias for each one.
provider "aws" {
alias = "us-east-1"
region = "us-east-1"
}
provider "aws" {
alias = "us-east-2"
region = "us-east-2"
}
provider "aws" {
alias = "ca-central-1"
region = "ca-central-1"
}
Because of these alias
directives, there is no default aws
provider at the
top level of the project. We'll need to specify which provider we need when
when calling for any aws
resource
or data
object at the top level.
Top-level main.tf
calls the per-region
module three times (once for each
region), and passes a different provider each time.
The per-region
module doesn't directly create any resources. Rather, it exists
only to call other modules: Modules which are not aware that work is being done
in multiple regions, use only a default provider, and do not expect to need to
pick their configuration data out of a per-region map. Y'know, regular terraform
stuff.
To better understand the purpose of the per-region
module, consider the
contents of variable.tf
:
variable "cidr" {
default = {
us-east-1 = "172.20.0.0/24"
us-east-2 = "172.20.1.0/24"
ca-central-1 = "172.20.2.0/24"
}
}
Top-level main.tf
and the per-region
module know the cidr
values for all
three regions, but the vpc
module (which doesn't know it's a clone) is
expecting something more like:
variable "cidr" { default = "172.20.0.0/24" }
The per-region
problem solves that problem by looking up the correct element
from the cidr
map and passing only that value when calling vpc
:
module "vpc" {
source = "../vpc"
cidr = var.cidr[data.aws_region.current.name]
}
Additionally, because main.tf
called per-region
with only a
default/un-aliased aws
provider, there's no confusion about which provider to
use when vpc
is doing its work.
In a real project, vpc
would be just one of many single-region modules called
by per-region
. The per-region
module isn't strictly required (main.tf
could call vpc
directly with the correct variables), but as modules and
resources bloom, things get crowded fast, and I find the per-region
abstraction helpful.
Top-level main.tf
also calls the vpc-peering
module. Like the vpc
module,
it also winds up getting called three times. vpc-peering
differs in that it
knows about multiple providers/regions because creating a VPC peering connection
requires doing coordinated work on both ends of the connection (in different
regions). It does not need to be "protected" from this information by an
intermediate module.
Calling the vpc-peering
looks like:
module "peering_1" {
source = "./vpc-peering"
accepting_vpc_id = module.us_east_2.vpc_id
requesting_vpc_id = module.us_east_1.vpc_id
providers = {
aws = aws.us-east-2 # Default aws provider for module
aws.requesting = aws.us-east-1 # Named/aliased provider for module
}
}
Note that we're passing two providers to the module this time. Like before,
one provider serves as the aws
default, but the second one is aliased
aws.requesting
, and will need to be explicitly referenced when used within the
module. There is no requirement to pass a default provider. Aliasing both would
have worked as well. But aliasing only a single module saves some typing in the
data
and resource
stanzas within the module.