Terraform Basics – Week 6: Building and Using Your First Terraform Module
Source: Dev.to
Recap of Previous Weeks
Over the past five weeks, we built a small but realistic Azure environment while learning the core Terraform concepts. We started by deploying our first VM, then introduced variables and tfvars files to make the configuration more flexible. We added security with NSGs and dynamic blocks, and finally exposed useful information through output values.
Here’s the visual representation of the infrastructure we have so far:
Folder structure in Visual Studio Code:
Key files in the project folder
nsg.tf– Network Security Group and its association.outputs.tf– Output values.providers.tf– AzureRM provider block.resource-group.tf– Resource group definition.terraform.tfvars– Variable values for the project.variables.tf– Variable definitions.virtual-machine.tf– Virtual machine and NIC resources.virtual-network.tf– Virtual network and subnet resources.
At this point we have a fully working, parameterized, and secure VM deployment. As the configuration grows, repeated patterns become evident, making management of multiple similar resources difficult. This is the perfect time to introduce Terraform modules.
Previous weeks
GitHub repository for the series:
What are Terraform Modules and Why Do We Use Modules in Terraform?
A Terraform module is a collection of resources grouped together so they can be reused as a single unit. Instead of rewriting the same VM, NIC, or NSG blocks for each deployment, we define them once inside a module and call that module whenever we need another instance.
The folder containing your main Terraform files is the root module. Any sub‑folder that contains .tf files becomes a child module and cannot run on its own. The root module supplies inputs, and the child module returns outputs. This relationship enables clean reuse of infrastructure patterns without duplicating code.
Modules become essential in real‑world environments. Imagine a team managing ten VMs across development, staging, and production. Without modules, each environment would contain its own copy of the same resource blocks. A simple change—such as updating tags or adjusting an NSG rule—must be repeated in multiple places, leading to drift and increased maintenance effort.
By centralizing the logic in a module, we achieve:
- Consistency across environments.
- Reduced duplication.
- Easier maintenance and scaling of infrastructure.
Understanding Module Inputs and Outputs
Module Inputs
Inputs are the values a module needs to create its resources. They are defined as variables inside the module, and the root module supplies the actual values when invoking the module. For a VM module, typical inputs include:
- Resource group name and location
- Subnet ID
- VM size
- Admin credentials
- NIC settings
- Allowed ports
Module Outputs
Outputs expose values from the child module back to the root module. This allows the root configuration to use information created inside the module without directly accessing its internal resources. Common outputs for a VM module are:
- VM ID
- NIC name
- Private IP address
These outputs can be referenced elsewhere in the configuration or displayed after deployment.
Creating Our First Terraform Module
Note: This is a refactoring of existing code into a reusable module rather than building a module from scratch. It mirrors a common real‑world scenario where a set of resources is turned into a reusable component.
We will move the VM, NIC, and NSG resources into a modules/vm folder while keeping the resource group, virtual network, and subnet in the root configuration. The existing variables.tf, outputs.tf, and terraform.tfvars remain in the root and will continue to provide values to the module.
Step 1: Create the module folder structure
modules/
└── vm/
├── main.tf
├── variables.tf
└── outputs.tf
Step 2: Populate main.tf in the module
Copy the resource blocks for the virtual machine, NIC, NSG, and NSG association from the original virtual-machine.tf and nsg.tf files into modules/vm/main.tf.
(The image shows the copied resource definitions.)
Step 3: Define module variables
Create modules/vm/variables.tf and declare all inputs the module requires (e.g., resource_group_name, subnet_id, vm_size, etc.). Use the same variable names and types that were previously defined in the root configuration.
Step 4: Define module outputs
Create modules/vm/outputs.tf to expose useful values such as the VM ID, NIC name, and private IP address.
Step 5: Call the module from the root configuration
In the root folder, add a module block (e.g., in main.tf or a dedicated vm-module.tf file) that references the new module and passes the required variables:
module "vm" {
source = "./modules/vm"
resource_group_name = var.resource_group_name
location = var.location
subnet_id = azurerm_subnet.main.id
vm_size = var.vm_size
admin_username = var.admin_username
admin_password = var.admin_password
allowed_ports = var.allowed_ports
}
The root’s existing outputs.tf can now reference the module’s outputs, e.g.:
output "vm_private_ip" {
value = module.vm.private_ip
}
With the module in place, the overall configuration remains functional while gaining the benefits of reuse and easier maintenance.


