Devops

Terraform Modules Using a Submodule: Practical Patterns

April 7, 2026
Published
#cloud#devops#infrastructure-as-code#modules#terraform

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:

TEXT
1modules/
2  vpc/
3    main.tf
4    variables.tf
5    outputs.tf
6    modules/
7      network/
8      security/
9      routing/
10

Calling submodules inside a module

Inside your main vpc module, you can reference submodules using relative paths:

TEXT
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}
18

Here’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:

TEXT
1# root module
2module "vpc" {
3  source = "./modules/vpc"
4
5  cidr_block = "10.0.0.0/16"
6}
7
TEXT
1# vpc/variables.tf
2variable "cidr_block" {}
3
TEXT
1# vpc/modules/network/main.tf
2variable "cidr_block" {}
3
4resource "aws_vpc" "this" {
5  cidr_block = var.cidr_block
6}
7

This 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.

TEXT
1# submodule output
2output "vpc_id" {
3  value = aws_vpc.this.id
4}
5
TEXT
1# main module output
2output "vpc_id" {
3  value = module.network.vpc_id
4}
5

Without 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:

JSON
1module "network" {
2  source = "git::https://github.com/org/network-module.git"
3}
4

But 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:

TEXT
1module "vpc" {
2  source = "company/vpc/aws"
3
4  cidr_block = "10.1.0.0/16"
5}
6

Behind 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.

Comments

Leave a comment on this article with your name, email, and message.

Loading comments...

Similar Articles

More posts from the same category you may want to read next.

Share: