Terraform 1.8 Module Composition: Advanced Techniques for Multi-Cloud Infra
As infrastructure becomes increasingly multi-cloud, managing complexity in Terraform is critical. Terraform 1.8 enhances module composition and provides powerful constructs like for_each, dynamic blocks, and workspaces to enable reusable, DRY (Don’t Repeat Yourself) infrastructure code.
In this guide, you’ll learn advanced techniques for composing Terraform modules in a multi-cloud environment, how to manage per-environment configurations, and apply reusable logic across AWS, Azure, and GCP.
🚀 What’s New in Terraform 1.8?
Terraform 1.8 brings enhancements to:
- Module metadata and validation
- Improved
for_eachin module blocks - Scoped outputs and input schema validation
- Cleaner workspace-aware designs
🧱 Organizing a Multi-Cloud Structure
Let’s start with a folder structure for a multi-cloud setup:
infra/ ├── modules/ │ ├── aws/ │ ├── azure/ │ └── gcp/ ├── environments/ │ ├── dev/ │ ├── staging/ │ └── prod/ ├── main.tf ├── variables.tf └── terraform.tfvars
This approach separates provider-specific modules and allows per-environment customization using workspaces.
🔄 Using for_each to Instantiate Modules per Environment or Region
Terraform 1.8 allows for_each over module blocks — ideal for deploying similar infra in multiple regions or clouds.
variable "regions" {
type = list(string)
default = ["us-east-1", "us-west-2"]
}
module "aws_vpc" {
for_each = toset(var.regions)
source = "../modules/aws"
region = each.key
cidr = "10.${index(var.regions, each.key)}.0.0/16"
}
This deploys a VPC module in each region without duplicating code.
🧬 Dynamic Blocks for Reusable Nested Configurations
Use dynamic blocks for optional or conditional nested resources. Here’s an example with security group rules:
resource "aws_security_group" "web" {
name = "web-sg"
description = "Security group for web servers"
vpc_id = var.vpc_id
dynamic "ingress" {
for_each = var.allow_ssh ? [22] : []
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
}
This will only add an SSH rule if allow_ssh is true.
🗂️ Workspaces for Environment Separation
Workspaces enable isolated state per environment (dev, staging, prod):
terraform workspace new dev terraform workspace select dev
In your code:
variable "env" {
default = terraform.workspace
}
Use this env variable to dynamically name resources or load env-specific variables.
Example:
resource "aws_s3_bucket" "logs" {
bucket = "my-logs-${var.env}"
}
📦 Writing a Cloud-Agnostic Module Interface
Design modules to accept a provider alias and other generic inputs:
provider "aws" {
alias = "east"
region = "us-east-1"
}
module "network" {
source = "./modules/aws"
providers = { aws = aws.east }
vpc_cidr = "10.0.0.0/16"
availability_zones = ["us-east-1a", "us-east-1b"]
}
You can use similar interfaces for Azure and GCP modules, allowing multi-cloud abstraction with shared inputs.
🛡️ Validating Module Inputs (Terraform 1.8+ Feature)
Define strict validation rules:
variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
validation {
condition = can(cidrsubnet(var.vpc_cidr, 8, 0))
error_message = "The vpc_cidr must be a valid CIDR block."
}
}
This prevents runtime failures by enforcing configuration rules early.
📊 Visualizing Multi-Cloud Composition
Use Terraform Graph or third-party tools like:
📁 Example Use Case: AWS + GCP VPC Deployment
module "aws_network" {
source = "../modules/aws"
region = "us-east-1"
cidr = "10.0.0.0/16"
}
module "gcp_network" {
source = "../modules/gcp"
region = "us-central1"
cidr = "10.1.0.0/16"
}
Both modules implement a unified interface and are deployed independently with shared logic.
🧠 Best Practices Recap
✅ Use for_each and dynamic for scalable resource generation
✅ Leverage workspaces for isolated environments
✅ Validate module inputs early
✅ Abstract provider-specific logic in modules
✅ Keep modules small, composable, and testable



