At some point, a single Terraform module stops being enough. You start with a neat abstraction—say, a VPC module—and then realize it needs routing, security groups, and maybe logging. Cramming everything into one module makes it rigid. Splitting it intelligently introduces a powerful pattern: modules that use submodules.
Let’s break this down with a practical lens, not just theory.
What does “module using a submodule” mean?
In Terraform, a module can call another module internally. This is often referred to as a nested module or submodule usage.
Instead of exposing every resource directly, your main module becomes an orchestrator that composes smaller, focused submodules.
A simple mental model
- Main module = coordinator
- Submodules = specialized building blocks
Start with a concrete example
Imagine you’re building a reusable AWS VPC module. Instead of putting everything in one place, you split it:
- network → VPC + subnets
- security → security groups
- routing → route tables
Your folder structure might look like this:
1modules/
2 vpc/
3 main.tf
4 variables.tf
5 outputs.tf
6 modules/
7 network/
8 security/
9 routing/
10Calling submodules inside a module
Inside your main vpc module, you can reference submodules using relative paths:
1module "network" {
2 source = "./modules/network"
3
4 cidr_block = var.cidr_block
5}
6
7module "security" {
8 source = "./modules/security"
9
10 vpc_id = module.network.vpc_id
11}
12
13module "routing" {
14 source = "./modules/routing"
15
16 vpc_id = module.network.vpc_id
17}
18Here’s where it gets interesting: outputs from one submodule feed directly into another. Terraform handles the dependency graph automatically.
Why use submodules instead of one big module?
A common mistake developers make is overloading a module until it becomes impossible to maintain.
Breaking it into submodules gives you:
- Separation of concerns — each submodule has a clear responsibility
- Reusability — submodules can be reused independently later
- Cleaner testing — smaller units are easier to validate
- Better readability — no 1,000-line main.tf files
Passing variables through layers
One subtle challenge with Terraform modules using submodules is variable flow.
You often pass variables from the root → main module → submodule:
1# root module
2module "vpc" {
3 source = "./modules/vpc"
4
5 cidr_block = "10.0.0.0/16"
6}
71# vpc/variables.tf
2variable "cidr_block" {}
31# vpc/modules/network/main.tf
2variable "cidr_block" {}
3
4resource "aws_vpc" "this" {
5 cidr_block = var.cidr_block
6}
7This chaining is explicit by design. Terraform avoids hidden magic, which keeps things predictable but requires discipline.
Output propagation pattern
If a submodule produces a value needed outside, you must bubble it up.
1# submodule output
2output "vpc_id" {
3 value = aws_vpc.this.id
4}
51# main module output
2output "vpc_id" {
3 value = module.network.vpc_id
4}
5Without this step, the root module cannot access submodule outputs.
When submodules become overkill
Not every module needs submodules. Over-structuring can slow you down.
Watch for these signs:
- You only have 2–3 resources total
- Submodules don’t get reused anywhere else
- Passing variables becomes more complex than the logic itself
In those cases, a single module is usually enough.
Remote submodules vs local submodules
So far, we’ve used local paths (./modules/network). You can also reference remote sources:
1module "network" {
2 source = "git::https://github.com/org/network-module.git"
3}
4But mixing remote submodules inside a reusable module introduces versioning complexity. A safer pattern is:
- Keep submodules local within a module
- Version the parent module as a unit
Design tips that make this pattern work
- Keep submodules small — one responsibility each
- Avoid circular dependencies — Terraform won’t allow them anyway
- Name modules clearly — “network”, “security”, not vague labels
- Expose only necessary outputs — don’t leak everything
- Document variable expectations — especially when chaining values
A quick real-world use case
In larger teams, a platform team might publish a vpc module that internally uses submodules. Application teams never see the complexity—they just consume:
1module "vpc" {
2 source = "company/vpc/aws"
3
4 cidr_block = "10.1.0.0/16"
5}
6Behind the scenes, that module composes networking, security, and routing submodules. This keeps the interface simple while maintaining flexibility internally.
Final thought
Terraform modules using submodules are less about syntax and more about design discipline. Used well, they help you scale infrastructure code without turning it into a monolith. Used poorly, they just add layers of indirection.
The sweet spot is simple: break things apart only when it makes your system easier to understand, reuse, and evolve.