You already know that a module is a box with inputs and outputs (subchapter 18.1). Now we’ll see how it’s actually used: how you pass values to it, how you collect its results, and how you connect several modules together to compose a complete infrastructure. This is the practical part that turns modules into a powerful tool.

Calling a Module

To use a module, you use a module block. You specify where it is (source) and pass it the values for its input variables:

module "my_network" {
  source = "./module-vpc"      # where the module is

  # values for the module's input variables:
  cidr_vpc = "10.0.0.0/16"
  name     = "store-project"
}

This is like “calling the recipe” from the previous subchapter, specifying the concrete ingredients. Terraform will take the module and create its resources with these values.

Notice how powerful this is: with just those few lines, you create the entire VPC with subnets, gateway, and routes that the module encapsulates. The complexity is hidden; you just pass two values.

Reusing the Same Module Multiple Times

Like a function, you can call the same module multiple times with different values. For example, to create the network for three environments:

module "dev_network" {
  source   = "./module-vpc"
  cidr_vpc = "10.0.0.0/16"
  name     = "development"
}

module "prod_network" {
  source   = "./module-vpc"
  cidr_vpc = "10.1.0.0/16"
  name     = "production"
}

A single module, two independent networks, without duplicating code. If you improve the module, both networks benefit.

Collecting a Module’s Outputs

The module returns information through its outputs (subchapter 18.1). To use that information from outside, you access it with module.<name>.<output>:

module.my_network.vpc_id
│      │       │
│      │       └── the output defined by the module
│      └────────── the name you gave it when calling it
└───────────────── the keyword "module"

For example, if you want to display the ID of the VPC created by the module:

output "my_vpc_id" {
  value = module.my_network.vpc_id
}

Connecting Modules: Dependencies

Here’s where the real magic of composition happens. You can connect the output of one module to the input of another, just like you connected resources in subchapter 12.4. This allows you to build complex architectures by composing small modules.

Imagine two modules: one creates the network (module-vpc) and another creates servers (module-servers). The servers module needs to know which VPC and subnet to use. You pass that information using the outputs of the network module:

module "my_network" {
  source   = "./module-vpc"
  cidr_vpc = "10.0.0.0/16"
  name     = "store"
}

module "my_servers" {
  source = "./module-servers"

  # we connect: the output of the network module goes into the servers module
  vpc_id    = module.my_network.vpc_id        # ← output from one module
  subnet_id = module.my_network.subnet_id     #    as input to another
}
   ┌─ module "my_network" ─┐         ┌─ module "my_servers" ─┐
   │  creates the VPC      │ ──────► │  creates servers      │
   │  output: vpc_id       │ vpc_id  │  INSIDE that VPC      │
   └───────────────────────┘         └───────────────────────┘

Dependencies Are Inferred Automatically (Again)

Just like with resources (subchapter 12.4), this connection automatically creates the dependency: since the servers module uses an output from the network module, Terraform knows it must create the network first and then the servers. You don’t specify the order; it’s inferred from the connections. The declarative philosophy also works between modules.

Real-world example: a complete architecture is composed of several chained modules: a network module that produces the VPC, a database module that is placed in the network, an application module that uses the network and the database, and a load balancer module in front of the application. Each module is a reusable, well-defined piece; the outputs of some feed the inputs of others, and Terraform orchestrates everything in the correct order. It’s like building with Lego pieces: each piece is simple, but combined they form something big.

The Philosophy: Composing Small Pieces

This way of working—small, well-defined modules that are composed by connecting inputs and outputs—is at the heart of good Terraform design. Instead of a giant, unmanageable file, you have pieces:

  • Reusable: each module serves in many projects.
  • Understandable: each piece does one thing and is easy to understand.
  • Maintainable: you fix or improve a module and everyone using it benefits.
  • Composable: you connect them to form architectures as complex as you need.

What You Should Remember

  • To use a module, you use a module block with source (where it is) and the values for its input variables. It’s like “calling the recipe” with specific ingredients.
  • You can reuse the same module multiple times with different values (e.g., dev and production networks), without duplicating code.
  • The outputs of a module are read with module.<name>.<output> and allow you to use its information from outside.
  • You connect modules by passing the outputs of one as inputs to another; this automatically creates the dependency and the creation order (just like with resources).
  • The philosophy: compose small, reusable, and well-defined pieces (like Lego) to build large, maintainable, and understandable architectures.

In the next subchapter, we’ll see where to get modules: those you create (local) versus the public ones from the Terraform Registry, ready to use.

Cloud, AWS & Terraform — From Zero to Expert

Chapter 1 · What is cloud computing

Chapter 2 · The cloud market and major providers

Chapter 3 · Regions, availability zones and edge

Chapter 4 · Compute: EC2

Chapter 5 · Storage: S3

Chapter 6 · Networking: VPC

Chapter 7 · Identity and access: IAM

Chapter 8 · Managed databases

Chapter 9 · Why Infrastructure as Code

Chapter 10 · HCL: the Terraform language

Chapter 11 · Providers and state

Chapter 12 · Your first real infrastructure in Terraform

Chapter 13 · Load balancing and auto scaling

Chapter 14 · Serverless with Lambda

Chapter 15 · Messaging and events

Chapter 16 · Content delivery and DNS

Chapter 17 · Containers on AWS

Chapter 18 · Modules: reuse and composition

Chapter 19 · Workspaces and environment management

Chapter 20 · Remote backends and locking

Chapter 21 · Infrastructure testing

Chapter 22 · Terraform in CI/CD

Chapter 23 · Defense in depth

Chapter 24 · Observability: logs, metrics and traces

Chapter 25 · Cost optimization

Chapter 26 · High availability and disaster recovery

Chapter 27 · AWS Well-Architected Framework

Chapter 28 · Serverless architectures at scale

Chapter 29 · Data platforms on AWS

Chapter 30 · Multi-account and landing zones

Chapter 31 · Platform Engineering and Internal Developer Platform

Chapter 32 · Relevant AWS certifications

Chapter 33 · Projects to consolidate what you've learned

Chapter 34 · Resources and community

© Copyright 2024. All rights reserved