Multi-Region modules with count

I am trying to deploy hundreds of resources in two different regions (us-east-2, eu-central-1).

I had all my code written to have a separate module block for the resources in each region. For example here are snippets of my Cognito portion:

.
├── Modules
│   ├── Cognito
│   │   ├── Scripts
│   │   │   ├── cognito-generate-token.js
│   │   │   └── cognito-message.js
│   │   ├── cognito.tf
│   │   ├── lambda.tf
│   │   ├── main.tf
│   │   ├── sg.tf
│   │   └── var.tf
├── main.tf
└── var.tf

./main.tf:

################################################################################
# Terraform
################################################################################
terraform {
  required_version = ">= 1.5"

  backend "s3" {
    bucket = "projectcanary-terraform-state"
    key    = "utilities-v2.tfstate"
    region = "us-east-2"
  }

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

provider "aws" {
  alias  = "eu-central-1"
  region = "eu-central-1"
}

provider "aws" {
  alias  = "us-east-2"
  region = "us-east-2"
}

################################################################################
# Modules
################################################################################

module "cognito_eu" {
  source         = "./Modules/Cognito"
  contact        = var.contact
  region_mapping = var.region_mapping
  service        = var.service

  providers = {
    aws = aws.eu-central-1
  }
}

module "cognito_us" {
  source         = "./Modules/Cognito"
  contact        = var.contact
  region_mapping = var.region_mapping
  service        = var.service

  providers = {
    aws = aws.us-east-2
  }
}

./Modules/Cognito.main.tf:

################################################################################
# Terraform
################################################################################
terraform {
  required_providers {
    archive = {
      source  = "hashicorp/archive"
      version = "~> 2.0"
    }
    aws = {
      source = "hashicorp/aws"
      configuration_aliases = [
        aws
      ]
    }
  }
}

provider "aws" {
  alias  = "eu-central-1"
  region = "eu-central-1"
}

provider "aws" {
  alias  = "us-east-2"
  region = "us-east-2"
}

This worked as expected. Adding Cognito resources (as well as hundreds of others from different modules) in the two regions.

However, now I need to make a conditional in the module to only create the Cognito resources in the dev and prod workspaces. So I added the following:

./main.tf:

module "cognito_eu" {
  count = contains(["dev", "prod"], local.environment) ? 1 : 0

  source         = "./Modules/Cognito"
  contact        = var.contact
  region_mapping = var.region_mapping
  service        = var.service

  providers = {
    aws = aws.eu-central-1
  }
}

module "cognito_us" {
  count = contains(["dev", "prod"], local.environment) ? 1 : 0
  
  source         = "./Modules/Cognito"
  contact        = var.contact
  region_mapping = var.region_mapping
  service        = var.service

  providers = {
    aws = aws.us-east-2
  }
}

But I get the following:

│ The module at module.cognito_us is a legacy module which contains its own local provider configurations, and so calls to it may not use the count, for_each, or depends_on arguments.
│
│ If you also control the module "./Modules/Cognito", consider updating this module to instead expect provider configurations to be passed by its caller.

I tried going down to one Cognito module in main.tf with multiple configuration aliases declared in ./Modules/Cognito/main.tf. But doing that only wants to deploy in one region.

What am I doing wrong? I need to conditionally create multiple resources in different environments.

Hi @wblakecannon,

I think you may need to complete the maneuver of moving the provider configurations into the root module as a separate step before you use count and for_each, because Terraform tracks the relationships between resources and provider configurations in the state and updates those only when you apply a change using a different provider configuration address.

In practice that means switching to using the providers argument in the module block and removing the provider block from your child module first, applying that change in isolation, and then applying the rest.

In the second step you will also need to add a moved block for each module call to tell Terraform that you intended to rebind the existing module instance to the new address with [0] at the end, rather than to destroy the old objects and create new ones at the new addresses.

It appears all I have to do is comment out the providers section of ./Modules/Cognito/main.tf and according to the plan it seems to work, but I don’t know why.

Ahh, okay! I assumed from what you described that you’d moved the provider blocks from the child module into the root module, but I guess you’d just added provider blocks at the root but kept the ones inside the module.

I expect that if you hadn’t got the error you saw then you would’ve seen a different error about it not being valid to use the providers block to pass a provider configuration into an address that matches one the child module already explicitly defines: a particular provider configuration address can either have a provider block inline or have a configuration passed by the caller, but not both.

Removing the provider block from the child module made this valid both because now you have no provider blocks in your shared module (which is not allowed when using count or for_each, as the error message explained) and you’re passing in a suitable provider configuration from the caller using the providers argument.

Revisiting this. Same error but doing something different using the kreuzwerker docker provider and my head is spinning.

I need to be able to make docker images in multiple regions and just can’t figure this out.

The docker provider can’t be declared in the root because it wants a region:

provider "docker" {
  registry_auth {
    address  = format("%v.dkr.ecr.%v.amazonaws.com", var.aws_account_id, data.aws_region.current.name)
    username = data.aws_ecr_authorization_token.token.user_name
    password = data.aws_ecr_authorization_token.token.password
  }
}

So how can I put that in the root if I need each module to deploy in a different region?

tree:

.
├── Modules
│   ├── CodePipeline
│   │   ├── Slack
│   │   │   ├── requirements.txt
│   │   │   ├── slack-messenger.py
│   │   │   └── slack-setup.sh
│   │   ├── Templates
│   │   │   ├── sam-deploy.yml.tftpl
│   │   │   ├── slack-init.yml.tftpl
│   │   │   ├── terraform-apply.yml.tftpl
│   │   │   └── validation.yml.tftpl
│   │   ├── codebuild.tf
│   │   ├── codepipeline.tf
│   │   ├── iam.tf
│   │   ├── main.tf
│   │   ├── s3.tf
│   │   ├── sg.tf
│   │   └── var.tf
│   ├── Cognito
│   │   ├── Scripts
│   │   │   ├── cognito-generate-token.js
│   │   │   └── cognito-message.js
│   │   ├── cognito.tf
│   │   ├── lambda.tf
│   │   ├── main.tf
│   │   └── var.tf
│   ├── Ecs
│   │   ├── WebApi
│   │   │   ├── Templates
│   │   │   │   └── container-definitions.json.tftpl
│   │   │   ├── acm.tf
│   │   │   ├── cloudwatch.tf
│   │   │   ├── ecr.tf
│   │   │   ├── ecs.tf
│   │   │   ├── glue.tf
│   │   │   ├── main.tf
│   │   │   ├── route53.tf
│   │   │   ├── var.tf
│   │   │   └── waf.tf
│   │   ├── ecs.tf
│   │   ├── main.tf | (kreuzwerker docker provider declared here)
│   │   ├── s3.tf
│   │   ├── sg.tf
│   │   ├── sns.tf
│   │   ├── var.tf
│   │   └── waf.tf
│   └── TimeScaleDB
│       ├── Scripts
│       │   ├── Dockerfile
│       │   ├── entry.sh
│       │   └── pgbouncer.ini
│       ├── Templates
│       │   ├── container-definitions.json.tftpl
│       │   ├── user_data-db-prm.sh.tftpl
│       │   └── user_data-db-stb.sh.tftpl
│       ├── cloudwatch.tf
│       ├── ec2.tf
│       ├── ecr.tf
│       ├── ecs.tf
│       ├── main.tf | (kreuzwerker docker provider attempted to be added here)
│       ├── route53.tf
│       ├── s3.tf
│       ├── sg.tf
│       ├── sns.tf
│       └── var.tf
├── Scripts
│   └── git-hash.sh
├── main.tf
└── var.tf

I have the docker provider declared in ./Modules/Ecs/main.tf

I tried to also put it in ./Modules/TimeScaleDB/main.tf and now I have the error.

I don’t understand how I can declare the docker provider in root because each module has its own associated region:

module "database_ec2_us" {
  count = contains(["demo", "dev"], local.environment) ? 1 : 0

  source = "./Modules/TimeScaleDB"

  ...
  ...

  providers = {
    aws = aws.us-east-2
  }
}
module "ecs_us" {
  source             = "./Modules/Ecs"
  
  ...
  ...

  providers = {
    aws = aws.us-east-2
  }
}

We have modules for us-east-2 and eu-central-1.

Do I fix it by adding this to root main.tf:

provider "docker" {
  alias = "us-east-2"

  registry_auth {
    address  = format("%v.dkr.ecr.%v.amazonaws.com", data.aws_caller_identity.current.account_id, "us-east-2")
    username = data.aws_ecr_authorization_token.token.user_name
    password = data.aws_ecr_authorization_token.token.password
  }
}

provider "docker" {
  alias = "eu-central-1"

  registry_auth {
    address  = format("%v.dkr.ecr.%v.amazonaws.com", data.aws_caller_identity.current.account_id, "eu-central-1")
    username = data.aws_ecr_authorization_token.token.user_name
    password = data.aws_ecr_authorization_token.token.password
  }
}

And then adding these to each module?:

providers = {
    aws    = aws.us-east-2
    docker = docker.us-east-2
  }
providers = {
    aws    = aws.eu-central-1
    docker = docker.eu-central-1
  }