Skip to main content

Building a Terraform module to simplify IaC

I’ve been building my infrastructure in my homelab with Terraform for a while now, one thing that has always been a pain is finding an IP address that’s not in use and then creating a DNS record in pfSense.

To solve the IP address problem I’ve decided to use Netbox. This will allow me to plan out the layout of the network and get available IPs using the API.

I’ve decided the first step to address this is to create a Terraform module, this will simplify the code. For a simple module that includes a Proxmox VM and IP from Netbox I did the below:

Create the structure for module:

mkdir my_module && cd my_module && touch variables.tf main.tf output.tf

Inside the main.tf we’ll put all the resources we want to create as part of this module (shortened):

terraform {
  required_providers {
    proxmox = {
      source = "Telmate/proxmox"
    }
    netbox = {
      source = "e-breuninger/netbox"
    }
  }
}

data "netbox_ip_range" "static_ip_range" {
  contains = "192.168.60.200/24"
}

resource "netbox_available_ip_address" "vm_ip" {
  ip_range_id = data.netbox_ip_range.static_ip_range.id
  description = "${var.vm_name}"
  status      = "active"
  dns_name    = "${var.vm_name}.lab.home"
}

resource "proxmox_vm_qemu" "vm" {
  #count = 1
  name  = "${var.vm_name}"
  desc  = "${var.vm_description}"

  # Node name has to be the same name as within the cluster
  # this might not include the FQDN
  target_node = "proxmox1"

  # The destination resource pool for the new VM
  onboot = true
  # The template name to clone this vm from
  clone = "${var.vm_template}"

  # Activate QEMU agent for this VM
  agent   = 1
  qemu_os = "l26"

  os_type = "cloud-init"
  cores   = 8
  sockets = 1
  vcpus   = 0
  cpu     = "host"
  memory  = var.vm_memory
  numa    = true
  scsihw  = "virtio-scsi-pci"
  hotplug = "disk,network,usb,memory,cpu"

  # Setup the network interface and assign a vlan tag: 256
  network {
    model  = "virtio"
    bridge = "vmbr0"
    tag    = "${var.vm_vlan}"
  }
  lifecycle {
    ignore_changes = [disk, network]

  }
  # Cloud Init Settings
  ipconfig0  = "ip=${netbox_available_ip_address.vm_ip.ip_address},gw=192.168.${var.vm_vlan}.1"
  ciuser     = "admin"
  cipassword = "ECRYPTED PASSWORD "
  sshkeys    = <<EOF
  ${var.ssh_key}
  EOF
}

There are some things to point out here. Firstly, you can see we have defined the terraform block again. I had problems when this wasn’t here. You can also see there are some fields that have variables, these will be used later to customise the infrastructure.

In the variables file we’ll define all those variables:

variable "vm_name" {
  type = string
}

variable "vm_description" {
  type = string
}

variable "vm_template" {
  type    = string
  default = "debian-bullseye-server"
}

variable "vm_vlan" {
  type    = number
  default = 60
}

variable "vm_memory" {
  type    = number
  default = 2048
}

variable "ssh_key" {
  type = string
  default = "PUBLIC SSH KEY HERE"
}

The outputs file exposes the infrastructure so it can be referenced elsewhere:

output "vm_id" {
  value = proxmox_vm_qemu.vm.id
}

output "vm_ip_address" {
  value = netbox_available_ip_address.vm_ip.ip_address
}

Once all these files are in place, create a new file where we’ll use the module:

module "test_vm" {
    source = "./my_module"
    vm_name = "testvm1"
    vm_description = "test vm using terraform module"
    vm_vlan = 60
}

Before we can plan and build, we need to init the module:

terraform init

Now we can build the infra using the new module. Review the plan before applying.

terraform plan
terraform apply

To complete this, I’ll be looking to implement DNS records into the IaC workflow.