Module Composition and Sources
The idea in one sentence
Section titled “The idea in one sentence”The source argument on a module block tells Terraform where to fetch that module’s code from, and real, non-trivial infrastructure gets built by composing several small modules together — one module’s output feeding directly into another module’s input variable.
Where a module’s code comes from: the source argument
Section titled “Where a module’s code comes from: the source argument”Every module block needs a source, and there are three kinds you will run into constantly. A local relative path points at a directory inside your own repository. The public Terraform Registry resolves a short <namespace>/<name>/<provider> address to a published, versioned module. A Git URL points directly at a repository, optionally pinned to a specific tag, branch, or commit with ?ref=.
# local relative pathmodule "storage" { source = "./modules/storage-account"}
# public Terraform Registrymodule "vnet" { source = "Azure/vnet/azurerm" version = "~> 5.0"}
# Git URL pinned to a specific tagmodule "vnet_from_git" { source = "git::https://github.com/acme/terraform-modules.git//vnet?ref=v1.4.0"}All three forms are just different answers to the same question: where does Terraform go get the .tf files for this child module. Nothing about how you write variables.tf, main.tf, or outputs.tf inside the module changes based on which kind of source a caller happens to use.
Composing modules: one output becomes another’s input
Section titled “Composing modules: one output becomes another’s input”A single flat configuration with hundreds of resources is hard to read and impossible to reuse. Real infrastructure is built from small, focused modules wired together through their outputs and inputs. A vnet module exposes the identifiers other modules need:
output "vnet_id" { description = "ID of the created virtual network" value = azurerm_virtual_network.this.id}
output "subnet_ids" { description = "IDs of the subnets" value = azurerm_subnet.this[*].id}A virtual-machine module declares the inputs it needs to launch into that network:
variable "vnet_id" { description = "ID of the virtual network to launch the VM into" type = string}
variable "subnet_id" { description = "ID of the subnet to launch the VM into" type = string}The root configuration is what actually wires them together, passing one module’s output straight into another module’s input:
# root main.tfmodule "vnet" { source = "./modules/vnet" address_space = "10.0.0.0/16"}
module "web_server" { source = "./modules/virtual-machine"
vnet_id = module.vnet.vnet_id subnet_id = module.vnet.subnet_ids[0]}Neither module needs to know the other exists. The vnet module has no idea a virtual-machine module will ever consume its outputs, and the virtual-machine module has no idea its vnet_id came from a module rather than a hardcoded string. The root configuration is the only place that knows both modules and connects them.
The public Terraform Registry as a productivity lever
Section titled “The public Terraform Registry as a productivity lever”Common patterns like “a virtual network with public and private subnets” have been solved many times already. The public Terraform Registry hosts well-maintained, widely-used community modules — the Azure organization’s modules, such as Azure/vnet/azurerm and Azure/naming/azurerm, being some of the most common starting points for Azure infrastructure — and reaching for one of these is usually a better use of your time than writing the equivalent from scratch.
module "vnet" { source = "Azure/vnet/azurerm" version = "~> 5.0"
resource_group_name = azurerm_resource_group.main.name vnet_name = "acme-app-vnet" address_space = ["10.0.0.0/16"] subnet_prefixes = ["10.0.1.0/24", "10.0.2.0/24"] subnet_names = ["private", "public"]}That said, “widely used” is not the same as “safe to trust blindly.” Before you build production infrastructure on top of a Registry module, read what it actually creates — its resources, its default values, and any role assignments it provisions on your behalf. A Registry module is still someone else’s code running with your Azure credentials.
flowchart LR
subgraph root["Root configuration"]
vnetmod["module vnet"]
vmmod["module web_server"]
end
subgraph vnet["Child module: vnet"]
vnetout1["output vnet_id"]
vnetout2["output subnet_ids"]
end
subgraph vm["Child module: virtual-machine"]
vmin1["var.vnet_id"]
vmin2["var.subnet_id"]
end
vnetmod --> vnetout1
vnetmod --> vnetout2
vnetout1 -->|module.vnet.vnet_id| vmin1
vnetout2 -->|module.vnet.subnet_ids[0]| vmin2
vmin1 --> vmmod
vmin2 --> vmmod