Automating VM Deployment in Proxmox Using OpenTofu (Terraform) and Cloud-Init Pt. 1

Automating VM Deployment in Proxmox Using OpenTofu (Terraform) and Cloud-Init Pt. 1
Note: This is Part 1 of a two-part series. In this post, we'll focus on the manual approach of downloading and preparing a cloud-init-enabled Ubuntu image in Proxmox that can be used as a base template for cloning. In Part 2, we’ll dive into using OpenTofu to automate the deployment of multiple VMs using infrastructure as code.

Provisioning multiple virtual machines manually in Proxmox can be time-consuming, especially when working with cloud-init-enabled templates. In this blog post, I'll walk you through how I use OpenTofu to automate the creation of cloud-init-ready VMs for both Linux and Windows in Proxmox, with a few reusable templates and code snippets.


Why Cloud-Init?

Cloud-init is a widely used method for early initialization of cloud instances. It allows us to:

  • Preconfigure users, SSH keys, and passwords
  • Set hostname and network settings
  • Install packages or run scripts during boot

Combined with OpenTofu and Proxmox, it creates a powerful automation stack.


Step 1: Download and Prepare Cloud-Init Images

Ubuntu 24.04

Run this directly on your Proxmox node

mkdir -p /var/lib/vz/template/qcow2
cd /var/lib/vz/template/qcow2
wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img

# install libguestfs-tools
sudo apt update -y && sudo apt install libguestfs-tools -y

# customize the image, install guest-agent, set a default root password and clear the machine-id
virt-customize -a /var/lib/vz/template/qcow2/noble-server-cloudimg-amd64.img \
  --install qemu-guest-agent \
  --root-password password:Password123 \
  --run-command "echo -n > /etc/machine-id"

This downloads an official CI enabled Ubuntu image, installs the guest agent, sets a root password for initial logins (not recommended in prod), and clears the machine ID to ensure uniqueness per VM.

Windows Server 2012 R2

You can download the evaluation image for Windows Server 2012 R2 from Cloudbase.it. Unfortunately, there is no ready-made image for Windows Server 2016, and while 2012 R2 is end-of-life, it should be sufficient for non-production testing environments.


Step 2: Create Cloud-Init Templates in Proxmox

We’ll now turn these image into Proxmox VMs template with cloud-init support.

You need to adjust these for your environment:

  • storage -> mine is called zfs_vms
  • nameserver
  • search domain

Please see the comments for more info.

Ubuntu 24.04 Template Creation

# This will just create a new VM. You can call it a shell for our template.
qm create 9502 --name "ubuntu-2404-ci-template" --memory 8192 --cores 4 --net0 virtio,bridge=vmbr0
# Here we import the disk image that we just downloaded
qm importdisk 9502 /var/lib/vz/template/qcow2/noble-server-cloudimg-amd64.img zfs_vms
# Next we attach the cloned disk to the VM
qm set 9502 --scsihw virtio-scsi-single --virtio0 zfs_vms:vm-9502-disk-0,cache=writeback,discard=on
# Set the cloned disk as bootdisk
qm set 9502 --boot c --bootdisk virtio0
# Add an additional disk for cloudinit. This will hold our cloudinit disk image.
qm set 9502 --scsi1 zfs_vms:cloudinit
# Enable the qemu guest agent
qm set 9502 --agent enabled=1
# Resize the cloned disk to 30GB
qm resize 9502 virtio0 +27748M
qm set 9502 --cpu cputype=host
qm set 9502 --ostype l26
qm set 9502 --balloon 4096
# All packages will be automatically upgrade upon first boot
# When using this make sure to start the VM once before converting it to a template
# qm set 9502 --ciupgrade 1
# Create a local user called ansible
qm set 9502 --ciuser ansible
# Set the password to "ansible" for the local user
qm set 9502 --cipassword ansible
qm set 9502 --ipconfig0 ip=dhcp
qm set 9502 --nameserver 192.168.11.254
qm set 9502 --searchdomain domain.local
# You need to create this file prior to running the command. You can skip this completely if you prefer using passwords.
qm set 9502 --sshkeys /mnt/pve/dir01/snippets/authorized_keys
# Start the VM once when using ciupgrade 1
# qm start 9502
# Finally we turn the VM into a template
qm template 9502

Windows Server 2012 R2 Template Creation

I'm still struggling to get this running. I'm not sure yet but my VM is stuck in a boot loop. I used this image in OpenStack before and it worked just fine. I will keep trying and update the post at a later stage.

qm create 9503 --name "win2k12-r2-ci-template" --memory 4096 --cores 2 --net0 virtio,bridge=vmbr0
qm importdisk 9503 /var/lib/vz/template/qcow2/windows_server_2012_r2_standard_eval_kvm_20170321.qcow2 zfs_vms
qm set 9503 --scsihw virtio-scsi-single --virtio0 zfs_vms:vm-9505-disk-0,cache=writeback,discard=on
qm set 9503 --boot c --bootdisk virtio0
qm set 9503 --scsi1 zfs_vms:cloudinit
qm set 9503 --agent enabled=1
qm resize 9503 virtio0 +27748M
qm set 9503 --cpu cputype=host
qm set 9503 --ostype win8
qm set 9503 --balloon 4096
qm set 9503 --ciuser ansible
qm set 9503 --cipassword ansible
qm set 9503 --ipconfig0 ip=dhcp
qm set 9503 --nameserver 192.168.11.254
qm set 9503 --searchdomain domain.local
qm template 9503

Step 3: Clone a VM from the Cloud-Init Templates

Once your templates are ready, you can manually clone new VMs from them using the following command:

Ubuntu 24.04

qm clone 9502 101 --name ubuntu-ci-test --full false
qm set 101 --ciuser ansible --ipconfig0 ip=dhcp --sshkeys /mnt/pve/dir01/snippets/authorized_keys
qm start 101

This creates a new VM (ID 101) cloned from the Ubuntu 24.04 cloud-init template (ID 9502). You can adjust the IP settings, user, and SSH key per VM.

Windows 2012 R2

qm clone 9503 102 --name win2k12r2-ci-test --full false
qm set 102 --ciuser ansible --ipconfig0 ip=dhcp
qm start 102

This creates a new VM (ID 102) cloned from the Windows 2012 R2 cloud-init template (ID 9503). You can adjust the IP settings, user, and SSH key per VM.

Both VMs are linked clones, meaning they are created much faster than full VMs. However, the template must remain intact; if the template is deleted or lost, the VMs will no longer function. You can simply change this by using "--full true".


Step 4: Use OpenTofu to Deploy VMs

With the templates in place, you can now use OpenTofu with the Telmate/proxmox provider to spin up multiple VMs, passing in cloud-init config (SSH keys, user, hostname, etc). You define:

  • clone source (your template ID)
  • ipconfig0, ciuser, sshkeys, etc.

A follow-up post will include my OpenTofu code and how to dynamically deploy VMs using variables and maps.


Wrapping Up

Using cloud-init-enabled templates in Proxmox, combined with OpenTofu, makes it easy to spin up consistent virtual machines quickly and reliably. Whether it’s for labs, dev environments, or internal automation, this workflow saves time and reduces human error.

Stay tuned for Part 2 where I’ll share the full OpenTofu setup!

Sources

Cloud Init Templates in Proxmox VE - Quickstart – Thomas-Krenn-Wiki
Die einfachste und effizienteste Art und Weise Linux VMs in Proxmox VE zu deployen ist der Einsatz von sogenannten Cloud-Init Images. Diese Images sind speziell angefertigte Installationsmedien, die von nahezu jeder bekannteren Linux-Distribution wie z.B. Debian, Ubuntu oder auch CentOS angeboten werden. Proxmox VE unterstützt Cloud-Init Deployments bereits seit einigen Versionen.
Privacy Policy Cookie Policy Terms and Conditions