Have you ever faced a situation where you needed to deploy identical infrastructure resources across multiple regions in a cloud provider like AWS or Azure?
The best solution to this is multi-region Terraform setups. In this blog, we'll walk through how to structure a Terraform repository to handle resources like S3 buckets, IAM roles, etc., across two or more regions, with reusability, scalability, and simplicity.
What we want?
We'll create a Terraform repository template where:
Infrastructure can be replicated across multiple regions.
The module logic is centralized and reusable.
Region-specific customizations are managed cleanly.
We'll walk through an example of provisioning an S3 bucket in two AWS regions: us-west-1
and us-west-2
.
Folder Structure
Here's the directory layout we'll be working with:
├── terraform/
│ ├── backend/
│ │ ├── us-west-1/
│ │ │ └── backend.tfvars
│ │ └── us-west-2/
│ │ └── backend.tfvars
│ ├── modules/
│ │ └── s3/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── output.tf
│ └── regions/
│ ├── us-west-1/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── provider.tf
│ │ └── output.tf
│ └── us-west-2/
│ ├── main.tf
│ ├── variables.tf
│ ├── provider.tf
│ └── output.tf
├── .gitignore
└── README.md
How to create re-usable terraform repository?
Let’s say we need to create an S3 bucket. Instead of writing this code for each region, we’ll write it once in a reusable module.
So in the modules/s3/variables.tf
, the resource looks like this:
resource "aws_s3_bucket" "bucket" {
bucket = var.bucket_name
tags = var.tags
force_destroy = true
}
This module expects two input values, so we define them in modules/s3/variables.tf
:
variable "bucket_name" {
type = string
description = "Name for the S3 bucket"
}
variable "tags" {
type = map(string)
description = "Tags for the resources to track cost for resources deployed"
}
Conclusion: This module doesn’t contain any hardcoded values, which makes it reusable.
Using the Module per Region
Now, let’s use this module for each region without making changes to the module itself.
In regions/us-west-1/main.tf
, we use the module like this:
module "s3" {
source = "../../modules/s3"
bucket_name = var.bucket_name
tags = var.tags
}
Where do bucket_name and tags come from?
This module expects two input values, so we define them in modules/s3/variables.tf:
We define them in that region’s variables.tf:
variable "root_var_bucket_name" {
type = string
description = "Name for the S3 bucket"
}
variable "root_var_tags" {
type = map(string)
description = "Tags for the resources to track cost for resources deployed"
}
You can define the actual values in your automation pipeline or in a .tfvars file.
For backend setup, the file backend/us-west-1/backend.tfvars could look like:
bucket = "my-terraform-state"
key = "us-west-1/terraform.tfstate"
region = "us-west-1"
dynamodb_table = "terraform-lock-table"
What to do if i need to make any configuration change, but to any particular region?
Now imagine that you need to change the S3 bucket name — but only for us-west-1.
You don't need to touch the module or duplicate the code. Just go to:
terraform/regions/us-west-1/variables.tf
And update the value for bucket_name. When you run Terraform for that specific region, the module will automatically pick up the new value.
cd terraform/regions/us-west-1
terraform init -backend-config=../../backend/us-west-1/backend.tfvars
terraform plan -var="bucket_name=my-west-bucket" -var="tags={Environment=\"dev\"}"
terraform apply
It doesn’t affect the setup in us-west-2 at all
Best Practices
- Avoid hardcoding values inside modules — always pass variables.
- Separate state per region to avoid conflicts.
- Use identical variable names across regions to standardize configuration.
- Centralize shared logic but isolate region-specific values.
- Add CI/CD automation for managing multiple environments smoothly.
Summary
This structure makes it easy to:
- Replicate infrastructure across regions
- Reuse modules without code duplication
- Make changes to individual regions independently
The same approach works not only for S3 buckets, but also IAM roles, DynamoDB, VPCs, and even across clouds like Azure or GCP.
Need more examples or want the full working repo on GitHub? Let me know in the comments!
Top comments (0)