Skip to content

Module Composition and Sources

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 path
module "bucket" {
source = "./modules/s3-bucket"
}
# public Terraform Registry
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
}
# Git URL pinned to a specific tag
module "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:

modules/vpc/outputs.tf
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:

modules/ec2-instance/variables.tf
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.tf
module "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
A vpc module's outputs feeding an ec2-instance module's inputs from the root configuration
Which three kinds of module source does this lesson describe
In the vpc and ec2-instance composition example, how does the ec2-instance module receive the subnet it should launch into
Does the vpc module in the example need to know that an ec2-instance module will consume its outputs
When does reaching for a public Terraform Registry module make more sense than writing your own from scratch