Packaging the mk2 vSphere deployment into a reusable Terraform module.
Problem#
The mk2 deployment uses variables, but the code lives in one project. If another team wants to use it, or you want to deploy a VM alongside other resources (DNS records, load balancers), you need to copy/paste. Modules solve this.
What Modules Are#
A Terraform module is a directory of .tf files. The directory you run terraform from is the
root module. Any directory you reference via source is a child module.
“Modules are reusable units of infrastructure. Write once, deploy everywhere. DRY applies to infra too.” - IaC principle
“Shiny.” - Firefly. Modular Terraform is shiny. Write once, reuse everywhere. No more copy-paste archaeology. Source: vsphere-vm-mk3
File Structure#
.
├── deployment.tf # Calls the module, defines root outputs
├── root_variables.tf # Variable declarations for tfvars
├── terraform.tfvars # Input values
└── modules/
└── vsphere-vm-mk3/
├── main.tf # Resources (same as mk2)
├── variables.tf # Module input variables
├── outputs.tf # Module outputs
├── versions.tf # Provider versions
├── README.md
└── LICENSEThe mk2 files move into modules/vsphere-vm-mk3/. The root module calls into them.
The Module Call#
deployment.tf references the module, passes variables through, and exposes outputs:
module "vsphere-vm-mk3" {
source = "./modules/vsphere-vm-mk3"
vsphere_user = var.vsphere_user
vsphere_password = var.vsphere_password
vsphere_server = var.vsphere_server
vsphere_allow_unverified_ssl = var.vsphere_allow_unverified_ssl
vsphere_datacenter = var.vsphere_datacenter
vsphere_datastore = var.vsphere_datastore
vsphere_compute_cluster = var.vsphere_compute_cluster
vsphere_network = var.vsphere_network
vsphere_template = var.vsphere_template
vm_name = var.vm_name
vm_domain_name = var.vm_domain_name
vm_num_cpus = var.vm_num_cpus
vm_memory = var.vm_memory
vm_ipv4_address = var.vm_ipv4_address
vm_ipv4_netmask = var.vm_ipv4_netmask
vm_ipv4_gateway = var.vm_ipv4_gateway
vm_dns_server_list = var.vm_dns_server_list
vm_dns_suffix_list = var.vm_dns_suffix_list
}
output "vm_ip_address" {
value = module.vsphere-vm-mk3.vm_ip_address
}
output "vm_hostname" {
value = module.vsphere-vm-mk3.vm_hostname
}Module outputs are accessed via module.<MODULE_NAME>.<OUTPUT_NAME>.
terraform init after adding or changing a module source. Terraform needs to
initialize the module before it can plan or apply.Why Modules Matter#
| Without Modules | With Modules |
|---|---|
| Copy/paste code between projects | source = "./modules/..." or a git URL |
| Changes require editing every copy | Change the module once, all consumers get the update |
| Hard to compose resources | Call multiple modules from one root |
Modules also make it possible to publish to the Terraform Registry for public or private sharing.
ref tag for versioning:
source = "git::https://github.com/org/repo.git//modules/vm?ref=v1.0.0". Without a ref,
Terraform pulls the latest commit on the default branch, which can break your deployment if the
module changes.Supplemental Files#
README#
Document your module inputs, outputs, and usage. terraform-docs can auto-generate this from your variable and output blocks.
LICENSE#
Required if sharing the module. Tells consumers how they can use your code. See opensource.guide/legal for help choosing a license.
Auto-generate module documentation from your code with terraform-docs:
terraform-docs markdown table ./modules/vsphere-vm-mk3/ > ./modules/vsphere-vm-mk3/README.mdIt reads your variables.tf and outputs.tf and produces a clean table of inputs, outputs,
types, defaults, and descriptions. Run it in CI so the docs never drift from the code.
What’s Next#
The VM module is reusable and composable. From here you can publish it to a private Terraform
registry, add CI with terraform validate and tflint, or layer it into larger deployments
alongside DNS, load balancers, and monitoring resources.