Admin

DevOps

Designing Terraform Multi-Cloud Modules for Enterprise Infrastructure

Master Terraform multi-cloud module design patterns. Build robust, scalable enterprise infrastructure across clouds with proven architectural best practices.

By Sujay SinghPublished: August 23, 202612 min read11 views✓ Fact Checked
Designing Terraform Multi-Cloud Modules for Enterprise Infrastructure
Designing Terraform Multi-Cloud Modules for Enterprise Infrastructure

Overview

The promise of cloud computing — agility, scalability, and cost-efficiency — has driven enterprises to adopt cloud-native strategies at an unprecedented pace. However, as organizations mature, many find themselves navigating a multi-cloud landscape, either by design to avoid vendor lock-in, for compliance reasons, or through mergers and acquisitions. Managing infrastructure across disparate cloud providers like Amazon Web Services (AWS), Microsoft Azure, and Google Cloud Platform (GCP) introduces significant operational complexity, inconsistency, and potential for misconfiguration.

This is where Infrastructure as Code (IaC) solutions, particularly Terraform by HashiCorp, become indispensable. Terraform allows engineers to define and provision infrastructure using a declarative configuration language (HCL). While powerful for single-cloud deployments, extending Terraform to a multi-cloud environment requires a thoughtful approach to module design. The goal is not just to provision resources, but to do so consistently, securely, and efficiently across different cloud ecosystems.

Multi-cloud module design patterns aim to encapsulate cloud-specific configurations into reusable, standardized building blocks. This approach abstracts away the underlying cloud provider's nuances, allowing for a more unified and streamlined deployment process. By adopting robust module design patterns, enterprises can achieve:

  • **Consistency:** Ensure identical environments (e.g., development, staging, production) across different clouds or regions.
  • **Reusability:** Develop modules once and use them across multiple projects, teams, or environments.
  • **Maintainability:** Simplify updates and changes by modifying a central module rather than scattered configurations.
  • **Scalability:** Rapidly provision new environments or expand existing ones with minimal effort.
  • **Improved Governance:** Enforce organizational standards, security policies, and compliance requirements through standardized modules.
  • **Reduced Operational Overhead:** Automate infrastructure provisioning, minimizing manual errors and accelerating deployment cycles.

This article will delve into practical, publication-ready design patterns for building effective Terraform multi-cloud modules, complete with real-world examples for AWS and Azure.

Prerequisites

Before diving into the implementation details, ensure you have the following prerequisites in place:

  • **Terraform CLI:** Install Terraform version 1.0 or newer. You can download it from the HashiCorp releases page.
    terraform -v

    Expected output similar to:

    Terraform v1.5.7
    on linux_amd64
  • **Cloud Provider Accounts:** Active accounts with AWS and Azure. For demonstration purposes, free tier accounts or trial subscriptions are sufficient.
  • **Cloud Provider CLIs:**
    • **AWS CLI:** Configured with appropriate programmatic access keys (IAM user with necessary permissions).
      aws configure

      Provide your AWS Access Key ID, Secret Access Key, default region (e.g., `us-east-1`), and default output format (e.g., `json`).

    • **Azure CLI:** Authenticated to your Azure subscription.
      az login

      This will open a browser window for authentication.

      az account show

      Verify your active subscription.

  • **Basic Terraform Knowledge:** Familiarity with Terraform HCL syntax, resources, data sources, variables, and outputs.
  • **Version Control System:** Git installed and configured for managing your Terraform configurations.
  • **Conceptual Understanding:** Basic knowledge of cloud networking (VPCs, subnets, security groups, virtual networks, resource groups) in both AWS and Azure.

Step-by-step Implementation: Designing Multi-Cloud Modules

The core challenge in multi-cloud module design is to balance abstraction with cloud-specific requirements. A common strategy involves creating cloud-agnostic "root" modules that orchestrate cloud-specific "child" modules. These child modules encapsulate the intricacies of each provider, offering a standardized interface to the root module.

Design Principles

  • **Abstraction:** Cloud-specific details (e.g., resource types, attribute names) should be hidden within child modules.
  • **Reusability:** Modules should be generic enough to be used across different environments (dev, stage, prod) and projects.
  • **Composition:** Build complex infrastructure by combining smaller, focused modules.
  • **Cloud-Agnostic Interfaces:** Define inputs and outputs for child modules that are as consistent as possible across providers, even if the underlying implementation differs significantly.
  • **Single Responsibility:** Each module should do one thing well (e.g., a network module, a compute module, a database module).

Example Scenario: Deploying a Multi-Cloud Network and Compute Environment

Let's design modules to provision a basic network and a compute instance in both AWS and Azure.

Project Structure

A typical multi-cloud Terraform repository structure might look like this:

├── environments/
│   ├── dev/
│   │   ├── aws/
│   │   │   └── main.tf
│   │   ├── azure/
│   │   │   └── main.tf
│   │   └── common.tfvars
│   ├── prod/
│   │   ├── aws/
│   │   │   └── main.tf
│   │   ├── azure/
│   │   │   └── main.tf
│   │   └── common.tfvars
├── modules/
│   ├── aws-network/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── aws-compute/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── azure-network/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── azure-compute/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
└── README.md

For this example, we'll focus on the `modules/` directory and how an environment-specific `main.tf` might call them.

1. AWS Network Module (`modules/aws-network`)

This module will create a VPC, subnets, and an Internet Gateway in AWS.

`modules/aws-network/variables.tf`

variable "vpc_cidr_block" {
  description = "The CIDR block for the VPC."
  type        = string
  default     = "10.0.0.0/16"
}

variable "public_subnet_cidrs" {
  description = "A list of CIDR blocks for public subnets."
  type        = list(string)
  default     = ["10.0.1.0/24", "10.0.2.0/24"]
}

variable "private_subnet_cidrs" {
  description = "A list of CIDR blocks for private subnets."
  type        = list(string)
  default     = ["10.0.101.0/24", "10.0.102.0/24"]
}

variable "environment" {
  description = "The environment name (e.g., dev, prod)."
  type        = string
}

variable "region" {
  description = "AWS region."
  type        = string
  default     = "us-east-1"
}

variable "tags" {
  description = "A map of tags to assign to resources."
  type        = map(string)
  default     = {}
}

`modules/aws-network/main.tf`

provider "aws" {
  region = var.region
}

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr_block
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = merge(var.tags, {
    Name        = "${var.environment}-vpc"
    Environment = var.environment
  })
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = merge(var.tags, {
    Name        = "${var.environment}-igw"
    Environment = var.environment
  })
}

resource "aws_subnet" "public" {
  count             = length(var.public_subnet_cidrs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.public_subnet_cidrs[count.index]
  availability_zone = "${var.region}${element(["a", "b", "c"], count.index)}"
  map_public_ip_on_launch = true

  tags = merge(var.tags, {
    Name        = "${var.environment}-public-subnet-${count.index + 1}"
    Environment = var.environment
  })
}

resource "aws_subnet" "private" {
  count             = length(var.private_subnet_cidrs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = "${var.region}${element(["a", "b", "c"], count.index)}"

  tags = merge(var.tags, {
    Name        = "${var.environment}-private-subnet-${count.index + 1}"
    Environment = var.environment
  })
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = merge(var.tags, {
    Name        = "${var.environment}-public-rtb"
    Environment = var.environment
  })
}

resource "aws_route_table_association" "public" {
  count          = length(aws_subnet.public)
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

`modules/aws-network/outputs.tf`

output "vpc_id" {
  description = "The ID of the created VPC."
  value       = aws_vpc.main.id
}

output "public_subnet_ids" {
  description = "A list of public subnet IDs."
  value       = [for s in aws_subnet.public : s.id]
}

output "private_subnet_ids" {
  description = "A list of private subnet IDs."
  value       = [for s in aws_subnet.private : s.id]
}

2. Azure Network Module (`modules/azure-network`)

This module will create a Resource Group, Virtual Network (VNet), and subnets in Azure.

`modules/azure-network/variables.tf`

variable "resource_group_name" {
  description = "The name of the resource group to create."
  type        = string
}

variable "location" {
  description = "The Azure region where resources will be deployed."
  type        = string
  default     = "eastus"
}

variable "vnet_address_space" {
  description = "The address space for the Virtual Network."
  type        = list(string)
  default     = ["10.1.0.0/16"]
}

variable "public_subnet_prefixes" {
  description = "A list of address prefixes for public subnets."
  type        = list(string)
  default     = ["10.1.1.0/24", "10.1.2.0/24"]
}

variable "private_subnet_prefixes" {
  description = "A list of address prefixes for private subnets."
  type        = list(string)
  default     = ["10.1.101.0/24", "10.1.102.0/24"]
}

variable "environment" {
  description = "The environment name (e.g., dev, prod)."
  type        = string
}

variable "tags" {
  description = "A map of tags to assign to resources."
  type        = map(string)
  default     = {}
}

`modules/azure-network/main.tf`

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "main" {
  name     = var.resource_group_name
  location = var.location

  tags = merge(var.tags, {
    Environment = var.environment
  })
}

resource "azurerm_virtual_network" "main" {
  name                = "${var.environment}-vnet"
  address_space       = var.vnet_address_space
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name

  tags = merge(var.tags, {
    Environment = var.environment
  })
}

resource "azurerm_subnet" "public" {
  count                = length(var.public_subnet_prefixes)
  name                 = "${var.environment}-public-subnet-${count.index + 1}"
  resource_group_name  = azurerm_resource_group.main.name
  virtual_network_name = azurerm_virtual_network.main.name
  address_prefixes     = [var.public_subnet_prefixes[count.index]]

  tags = merge(var.tags, {
    Environment = var.environment
  })
}

resource "azurerm_subnet" "private" {
  count                = length(var.private_subnet_prefixes)
  name                 = "${var.environment}-private-subnet-${count.index + 1}"
  resource_group_name  = azurerm_resource_group.main.name
  virtual_network_name = azurerm_virtual_network.main.name
  address_prefixes     = [var.private_subnet_prefixes[count.index]]

  tags = merge(var.tags, {
    Environment = var.environment
  })
}

`modules/azure-network/outputs.tf`

output "resource_group_name" {
  description = "The name of the created Azure Resource Group."
  value       = azurerm_resource_group.main.name
}

output "vnet_id" {
  description = "The ID of the created Virtual Network."
  value       = azurerm_virtual_network.main.id
}

output "public_subnet_ids" {
  description = "A list of public subnet IDs."
  value       = [for s in azurerm_subnet.public : s.id]
}

output "private_subnet_ids" {
  description = "A list of private subnet IDs."
  value       = [for s in azurerm_subnet.private : s.id]
}

3. Root Module Orchestration (e.g., `environments/dev/main.tf`)

This root module demonstrates how to call the cloud-specific network modules for a 'dev' environment.

`environments/dev/main.tf`

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

provider "aws" {
  region = var.aws_region
}

provider "azurerm" {
  features {}
  location = var.azure_location # Can also be set in module or resource
}

variable "environment" {
  description = "The environment name."
  type        = string
  default     = "dev"
}

variable "aws_region" {
  description = "AWS region for deployment."
  type        = string
  default     = "us-east-1"
}

variable "azure_location" {
  description = "Azure location for deployment."
  type        = string
  default     = "eastus"
}

variable "aws_vpc_cidr" {
  description = "CIDR block for AWS VPC."
  type        = string
  default     = "10.0.0.0/16"
}

variable "azure_vnet_cidr" {
  description = "CIDR block for Azure VNet."
  type        = list(string)
  default     = ["10.1.0.0/16"]
}

module "aws_network" {
  source = "../../modules/aws-network"

  vpc_cidr_block       = var.aws_vpc_cidr
  public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnet_cidrs = ["10.0.101.0/24", "10.0.102.0/24"]
  environment          = var.environment
  region               = var.aws_region
  tags = {
    Project = "MultiCloudApp"
  }
}

module "azure_network" {
  source = "../../modules/azure-network"

  resource_group_name   = "${var.environment}-rg-network"
  location              = var.azure_location
  vnet_address_space    = var.azure_vnet_cidr
  public_subnet_prefixes  = ["10.1.1.0/24", "10.1.2.0/24"]
  private_subnet_prefixes = ["10.1.101.0/24", "10.1.102.0/24"]
  environment           = var.environment
  tags = {
    Project = "MultiCloudApp"
  }
}

output "aws_vpc_id" {
  value = module.aws_network.vpc_id
}

output "azure_vnet_id" {
  value = module.azure_network.vnet_id
}

4. AWS Compute Module (`modules/aws-compute`)

This module will create an EC2 instance and a Security Group.

`modules/aws-compute/variables.tf`

variable "instance_name" {
  description = "Name for the EC2 instance."
  type        = string
}

variable "ami_id" {
  description = "AMI ID for the EC2 instance."
  type        = string
  default     = "ami-053b0d53c27927914" # Amazon Linux 2 AMI (HVM), SSD Volume Type, us-east-1
}

variable "instance_type" {
  description = "EC2 instance type."
  type        = string
  default     = "t3.micro"
}

variable "subnet_id" {
  description = "ID of the subnet to launch the instance into."
  type        = string
}

variable "vpc_id" {
  description = "ID of the VPC where the security group will be created."
  type        = string
}

variable "environment" {
  description = "The environment name."
  type        = string
}

variable "region" {
  description = "AWS region."
  type        = string
  default     = "us-east-1"
}

variable "tags" {
  description = "A map of tags to assign to resources."
  type        = map(string)
  default     = {}
}

`modules/aws-compute/main.tf`

provider "aws" {
  region = var.region
}

resource "aws_security_group" "web_sg" {
  name        = "${var.environment}-${var.instance_name}-sg"
  description = "Allow HTTP and SSH access"
  vpc_id      = var.vpc_id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = merge(var.tags, {
    Name        = "${var.environment}-${var.instance_name}-sg"
    Environment = var.environment
  })
}

resource "aws_instance" "web" {
  ami           = var.ami_id
  instance_type = var.instance_type
  subnet_id     = var.subnet_id
  vpc_security_group_ids = [aws_security_group.web_sg.id]
  associate_public_ip_address = true # Only for public subnets

  tags = merge(var.tags, {
    Name        = "${var.environment}-${var.instance_name}"
    Environment = var.environment
  })
}

`modules/aws-compute/outputs.tf`

output "instance_id" {
  description = "The ID of the EC2 instance."
  value       = aws_instance.web.id
}

output "public_ip" {
  description = "The public IP address of the EC2 instance."
  value       = aws_instance.web.public_ip
}

5. Azure Compute Module (`modules/azure-compute`)

This module will create an Azure Virtual Machine, Network Interface, and Network Security Group.

`modules/azure-compute/variables.tf`

variable "vm_name" {
  description = "Name for the Azure Virtual Machine."
  type        = string
}

variable "resource_group_name" {
  description = "The name of the resource group where the VM will be deployed."
  type        = string
}

variable "location" {
  description = "The Azure region for the VM."
  type        = string
}

variable "subnet_id" {
  description = "ID of the subnet to launch the VM into."
  type        = string
}

variable "admin_username" {
  description = "Administrator username for the VM."
  type        = string
  default     = "azureadmin"
}

variable "admin_password" {
  description = "Administrator password for the VM. (Use secrets manager in production)"
  type        = string
  sensitive   = true
}

variable "vm_size" {
  description = "Size of the Azure VM."
  type        = string
  default     = "Standard_B1s"
}

variable "image_publisher" {
  description = "Publisher of the VM image."
  type        = string
  default     = "Canonical"
}

variable "image_offer" {
  description = "Offer of the VM image."
  type        = string
  default     = "0001-com-ubuntu-server-focal" # Ubuntu Server 20.04 LTS
}

variable "image_sku" {
  description = "SKU of the VM image."
  type        = string
  default     = "20_04-lts-gen2"
}

variable "image_version" {
  description = "Version of the VM image."
  type        = string
  default     = "latest"
}

variable "environment" {
  description = "The environment name."
  type        = string
}

variable "tags" {
  description = "A map of tags to assign to resources."
  type        = map(string)
  default     = {}
}

`modules/azure-compute/main.tf`

provider "azurerm" {
  features {}
}

resource "azurerm_network_security_group" "web_nsg" {
  name                = "${var.environment}-${var.vm_name}-nsg"
  location            = var.location
  resource_group_name = var.resource_group_name

  security_rule {
    name                       = "SSH"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "HTTP"
    priority                   = 110
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "80"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  tags = merge(var.tags, {
    Name        = "${var.environment}-${var.vm_name}-nsg"
    Environment = var.environment
  })
}

resource "azurerm_public_ip" "web_ip" {
  name                = "${var.environment}-${var.vm_name}-public-ip"
  location            = var.location
  resource_group_name = var.resource_group_name
  allocation_method   = "Dynamic" # Use Static for production

  tags = merge(var.tags, {
    Name        = "${var.environment}-${var.vm_name}-public-ip"
    Environment = var.environment
  })
}

resource "azurerm_network_interface" "web_nic" {
  name
📧

Enjoyed this article?

Get articles like this delivered to your inbox daily. Join 10,000+ tech professionals.

Written By

Sujay Singh

Technology Expert / Cloud Architect at Virtual Venture covering AI, cloud computing, cybersecurity, and emerging tech trends.

Sources & References

• Official company announcements and press releases

• Industry reports from Gartner, IDC, and Statista

• Peer-reviewed research and technical documentation

• On-record statements from industry experts

Last verified: August 23, 2026

Fact-checked by TechNews Venture editorial team

Leave a Comment

Comments are moderated and will appear after review.