How to Build a Terraform Reusable Module
A Terraform module is just a directory of .tf files with declared variables. When the same resource block appears three times with slightly different names, that's the signal to extract it - one module, three small calls, no more copy-paste.
When to create a module
Copy-paste in Terraform is a common starting point: you need three services, so
you write three identical resource blocks and swap the name. It works until you
need to add a fourth, or change a setting in all three, or debug which paste is
the canonical one.
The rule of thumb: if you have the same resource pattern repeated two or more times with only a few values changing, extract it into a module.
Module directory layout
A module is nothing more than a directory containing .tf files. By convention it
has three files:
modules/service/
variables.tf # inputs the caller provides
main.tf # the actual resources, using var.*
outputs.tf # values the caller can read back
variables.tf - declare the inputs
variable "name" {
description = "Service name (used in the config file path)"
type = string
}
variable "enabled" {
description = "Whether the service is enabled"
type = bool
default = true
}
main.tf inside the module - use var.*
Inside the module every reference to the caller-provided values goes through var.:
resource "local_file" "config" {
content = "service=${var.name}\nenabled=${var.enabled}\n"
filename = "/tmp/config-${var.name}.conf"
}
resource "null_resource" "marker" {
triggers = {
config = local_file.config.content
}
}
outputs.tf - expose values to the root
output "config_path" {
description = "Path to the generated config file"
value = local_file.config.filename
}
Root main.tf - call the module three times
The root module shrinks to three small module blocks. Each call passes a
different name; everything else is encapsulated inside ./modules/service:
module "api" {
source = "./modules/service"
name = "api"
}
module "worker" {
source = "./modules/service"
name = "worker"
}
module "scheduler" {
source = "./modules/service"
name = "scheduler"
}
Re-run terraform init after creating the module directory - Terraform caches
module sources and won't pick up new ones without a fresh init. The plan output
will show resources namespaced under the module call:
module.api.local_file.config, module.worker.null_resource.marker, and so on.
The resource count stays the same; only the addresses change.
Reading a module output from the root
output "api_config_path" {
value = module.api.config_path
}
The root references module.<call_name>.<output_name>. This is how modules
expose their internals selectively - the caller sees only what outputs.tf
declares.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Extracting duplicated resource blocks into a modules/ directory with variables.tf / main.tf / outputs.tf
- Calling a local module with the
moduleblock and passing variables - Reading module outputs from the root configuration
FAQ
What is a Terraform module?
A module is a directory of .tf files - variables.tf declares inputs, main.tf holds the resources, outputs.tf exposes values to the caller. The root configuration calls it with a module block and a source path. Any directory with .tf files is a valid module.
When should I extract a Terraform module?
When the same resource pattern appears two or more times with only a few values changing. Extract the repeated block into a module, declare the varying values as variables, and call the module once per instance with different inputs.
Do I need to run terraform init again after creating a module?
Yes. Terraform caches module sources during init. After adding or moving a module directory, remove .terraform/ and .terraform.lock.hcl, then run terraform init again so Terraform discovers the new module path.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →