Michael Cooper


ClickOps Homelab to IAC Using OpenTofu & Ansible

When a hobby isn't fun anymore, maybe it's time to make it fun again.

Published on Mon Sep 14 2026

I love my homelab. I can run so much stuff on my Proxmox node. It’s mostly used for game servers for my friend group, but I’ve also got things like Kubernetes nodes and Home Assistant running on it. I’m still speccing out the cost of installing a NAS and other niceties so that I’ve got somewhere to store data that’s not just up in the cloud somewhere. It’s really fun to work on, but I have one major problem: I’m too afraid to upgrade anything, including Proxmox itself!

Enter Terraform

I’ve worked with Terraform, or rather OpenTofu, quite a bit at work to help migrate manual Azure resource management to an entirely GitOps process. I quickly fell in love with the concept of infrastructure-as-code (IaC) tools and started looking for other ways that we could use it. Not many more immediate opportunities to use this fancy new hammer at work. So, I made the next nail - my homelab.

After I was laid off, I needed some projects to keep me busy in between cleaning, game development, and relaxing. This felt like the perfect time to tackle that upgrade problem I’ve had with my homelab. Build some more marketable skills while getting something meaningful done? Couldn’t be better.

I decided to split up my project into two pieces, one public repository to house the modules I’d use to build the infrastructure, and a private repo where I’d consume these modules. The public modules live in iac-pve-public.

I created three modules, which each do the following:

The VM module is set up such that two VMs are created per configuration passed. Each one with their own disks. This approach smelled to me, but I kept with it for the sole purpose of making operating system upgrades easier. This first VM had no operating system, and was never to be booted. Its sole purpose is to own the data disk. The second VM’s duty is to act as the bootable machine which can be recreated/destroyed at will without risk of losing any critical data. Now, when the bootable VM is created, I can just automagically mount the data drive and start using it. We’ll see if this design comes back to bite me later.

Now, using it is pretty simple. Define a local set of virtual machines you want, and you’re pretty much ready to use the modules. I’ve lifted the following snippet from the v1.0.0 documentation in my modules repo:

module "keys" {
  source = "github.com/Maxxxxz/iac-pve-public/modules/key?ref=v1.0.0"

  for_each = {
    for k, v in local.vms : k => {
      key_name = v.name
    }
  }
  key_name = each.value.key_name
}

module "passwords" {
  source = "github.com/Maxxxxz/iac-pve-public/modules/password?ref=v1.0.0"

  for_each = {
    for k, v in local.vms : k => {
      password_name = v.name
    }
  }
  password_name = each.value.password_name
}

module "vms" {
  source = "github.com/Maxxxxz/iac-pve-public/modules/vm?ref=v1.0.0"

  depends_on = [module.keys, module.passwords]

  for_each = local.vms
  vm_config = each.value.config
  vm_name = each.value.name
  node_name = each.value.nodename
  user_account_public_key  = module.keys[each.key].public_key
  user_account_password    = module.passwords[each.key].password
}

locals {
    vms = {
        debian-test = {
            name = "debian-test"
            nodename = "test"
            config = {
                vm_tags = ["debian_server"]
                vm_description = "Test debian server."
                os_disk = {
                    disk_size_gb = 8
                    disk_interface = "scsi0"
                    disk_import_from = "local:import/your-debian-cloud-image.qcow2"
                    disk_datastore_id = "local-lvm"
                }
                data_disks = [
                    # Data Disk
                    {
                        disk_size_gb = 10
                        disk_interface = "scsi1"
                        disk_datastore_id = "local-lvm"
                    },
                ]
                cpu_sockets = 1
                cpu_type = "host"
                cpu_cores = 2
                user_account_username = "debian"
                network_vlan_id = {vlan id}
                memory_size_mb = {memory}
                ipv4_address = "{ip}/{mask}"
                ipv4_gateway = "{ip}"
            }
        },
        # Add more vms here.
    }
}

Once you apply, you should have your new VMs.

What about Ansible?

Now that we’ve got VMs creating automatically, we need some way to configure them. OpenTofu can’t do that, so we need to use a configuration-as-code (CaC) tool like Ansible. This will allow us to manage the intricate details inside of the VM.

I’m still in the process of building out different tasks, but primarily I’ll be using tasks to:

I’ll provide some Ansible examples later after I’ve got everything working.

One last problem…

I mentioned at the beginning of this post that I’m too afraid to upgrade anything. This is true, even when I have backups. Currently, I’m manually backing up data that I care about. Who knows when something might happen, and I lose the drives out of my homelab. I am not too good at following the 3-2-1 rule, however I at least keep copies of this data on multiple machines. The ideal solution to this problem would be to install a NAS, back up data to it, and pay for a cloud backup solution for my off-site, but with the cost of drives rising, I wince at the thought of buying any more than one or two 12TB hard drives. Eventually I’m going to have to, but that’s a little too big of a purchase to not talk about with the wife first… Now that I’m back to work, maybe those drives could be a back-to-work gift to myself?