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 "bucket" { source = "./modules/s3-bucket"}
# public Terraform Registrymodule "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 5.0"}
# Git URL pinned to a specific tagmodule "vpc_from_git" { source = "git::https://github.com/acme/terraform-modules.git//vpc?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 vpc module exposes the identifiers other modules need:
output "vpc_id" { description = "ID of the created VPC" value = aws_vpc.this.id}
output "private_subnet_ids" { description = "IDs of the private subnets" value = aws_subnet.private[*].id}An ec2-instance module declares the inputs it needs to launch into that network:
variable "vpc_id" { description = "ID of the VPC to launch the instance into" type = string}
variable "subnet_id" { description = "ID of the subnet to launch the instance 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 "vpc" { source = "./modules/vpc" cidr_block = "10.0.0.0/16"}
module "web_server" { source = "./modules/ec2-instance"
vpc_id = module.vpc.vpc_id subnet_id = module.vpc.private_subnet_ids[0]}Neither module needs to know the other exists. The vpc module has no idea an ec2-instance module will ever consume its outputs, and the ec2-instance module has no idea its vpc_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 VPC with public and private subnets” have been solved many times already. The public Terraform Registry hosts well-maintained, widely-used community modules — terraform-aws-modules/vpc/aws being one of the most common starting points for AWS networking — and reaching for one of these is usually a better use of your time than writing the equivalent from scratch.
module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 5.0"
name = "acme-app-vpc" cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"] private_subnets = ["10.0.1.0/24", "10.0.2.0/24"] public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]}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 IAM permissions it provisions on your behalf. A Registry module is still someone else’s code running with your AWS credentials.
flowchart LR
subgraph root["Root configuration"]
vpcmod["module vpc"]
ec2mod["module web_server"]
end
subgraph vpc["Child module: vpc"]
vpcout1["output vpc_id"]
vpcout2["output private_subnet_ids"]
end
subgraph ec2["Child module: ec2-instance"]
ec2in1["var.vpc_id"]
ec2in2["var.subnet_id"]
end
vpcmod --> vpcout1
vpcmod --> vpcout2
vpcout1 -->|module.vpc.vpc_id| ec2in1
vpcout2 -->|module.vpc.private_subnet_ids[0]| ec2in2
ec2in1 --> ec2mod
ec2in2 --> ec2mod