Introduction
Imagine you are an architect designing a neighborhood. You wouldn't draw every single house from scratch, brick by brick. Instead, you would create a blueprint for a "Standard House" and simply copy that blueprint 50 times.
In Terraform, these reusable blueprints are called modules.
Modules are the main way Terraform scales from small examples to real infrastructure codebases. They let teams package patterns once and reuse them many times.
Instead of repeating the same configuration again and again, you write it once inside a module and reuse it. Modules are important because real infrastructure codebases grow quickly and need structure just like application code.
In this lab, you will create a simple module that generates server configuration files, and then you will use that module to create two different servers.
Create Module Directory
First, we need a place to store our blueprints. Terraform modules are just regular folders with Terraform files inside them.
This is an important detail: a module is not a special new language feature in file format terms. It is simply a Terraform configuration packaged behind a reusable interface.
By convention, we usually create a folder named modules to keep things tidy. Inside that, we will create a folder for our specific "webserver" module.
Let's set up this structure:
cd ~/project
mkdir -p modules/webserver
Verify your work:
ls -R
You should see a modules directory containing a webserver directory. This webserver folder will act as a self-contained mini-project.
Define the Module
Now we need to write the blueprint itself.
A module works just like a root project: it has main.tf to define resources and variables.tf to accept inputs.
We will use a simple resource called local_file for this lab. It just creates a text file on disk. This is perfect for learning modules because it is fast and doesn't require complex cloud setups.
First, create the module's main.tf using nano:
nano modules/webserver/main.tf
Add this content. It tells Terraform to create a file with some specific text inside.
resource "local_file" "server_config" {
content = "Server Name: ${var.server_name}\nEnvironment: ${var.env}"
filename = "${path.root}/${var.server_name}-config.txt"
}
Save and exit nano (Ctrl+O, Enter, Ctrl+X).
Wait, var.server_name? We haven't defined that yet! A module needs to declare what inputs it accepts, just like a function in programming.
That function-like model is the right mental analogy. A module exposes inputs and outputs while hiding its internal resource details from the caller.
Create the module's variables.tf:
nano modules/webserver/variables.tf
Add these input definitions:
variable "server_name" {
description = "The name of the server"
type = string
}
variable "env" {
description = "The environment (e.g., dev, prod)"
type = string
default = "dev"
}
Save and exit nano.
Now our blueprint (module) is complete. It knows how to create a file, and it knows what information it needs (name and environment) to do the job.
Call the Module
Now we assume the role of the Site Manager. We want to build a server, but we don't want to worry about the details. We just want to "hire" the module to do it.
We do this by creating a module block in our root main.tf (the one in ~/project).
Create the root main.tf:
cd ~/project
nano main.tf
Add this content to "call" the module:
module "my_first_server" {
source = "./modules/webserver"
server_name = "web-01"
env = "production"
}
Remember to save and exit nano.
Let's decode this:
module "my_first_server": This is the name of this specific instance.source: Crucial! This tells Terraform where to find the blueprint folder (./modules/webserver).server_nameandenv: These are the inputs we are passing to the module.
This means a module call lets the root configuration describe intent at a higher level. Instead of retyping every low-level resource, you say "instantiate this reusable pattern with these inputs."
IMPORTANT: Whenever you add a new module, you must tell Terraform to prepare it. Run init to install the module:
terraform init
Now, apply the design:
terraform apply -auto-approve
Check the results. You should see a new file created by the module:
cat web-01-config.txt
Success! The module took our inputs ("web-01" and "production") and did the work for us.
Reuse the Module
This is where the magic happens. We want a second server. Instead of copying all the resource code, we just stamp the blueprint one more time.
Open your root main.tf again using nano:
nano main.tf
Use the arrow keys to move to the bottom of the file and append this block:
module "my_second_server" {
source = "./modules/webserver"
server_name = "web-02"
## We are not setting 'env' here, so it will use the default value "dev"
}
Save and exit nano.
Notice how clean this is? We just say "I want another server named web-02". We don't need to know how local_file works internally. We just use the interface.
Apply the changes:
terraform init
terraform apply -auto-approve
Verify that you now have two separate server files:
ls -l *-config.txt
And check that the second one used the default "dev" environment:
cat web-02-config.txt
You have just mastered the basic principle of reusable infrastructure!
Summary
Congratulations! You have taken a giant leap towards writing professional Terraform code.
In this lab, you learned:
- Modules: How to group code into reusable folders (your "blueprints").
- Inputs: How to use variables to make modules flexible.
- Calling Modules: How to use the
moduleblock to create instances of your blueprint. - Reuse: How to easily create multiple copies of infrastructure with minimal code.
Instead of a messy pile of bricks, you now have a set of reusable components. This helps you build faster, safer, and cleaner infrastructure.



