DEV Community

From a bare Proxmox host to Debian VMs in one command, with Terraform and cloud-init

Clicking through the Proxmox UI to create a VM is fine the first time. By the fifth VM you have five slightly different machines, and you are not sure which one has your SSH key or the right disk size.

This post shows the setup I use instead: a cloud-init template built once, and Terraform that creates every VM from a short list. Everything here is in a free repository, Rackplinth Lite (MIT). It was tested on Proxmox VE 9.2 with OpenTofu 1.13 and the bpg/proxmox provider 0.122.

The plan

  • Build a Debian 13 template from the official cloud image, with the QEMU guest agent inside.
  • Give Terraform an API token.
  • Describe the VMs in a map and run terraform apply.

Each VM comes up with a static IP and your SSH key, and password login is off. Terraform also writes an Ansible inventory, so you can start configuring the VMs right away.

Step 1: a template you can trust

Debian publishes cloud images with SHA512 checksums. The script downloads the image, checks it, and refuses to continue if the checksum does not match:

wget -q "$BASE/$IMAGE" "$BASE/SHA512SUMS"
grep " $IMAGE\$" SHA512SUMS | sha512sum -c -

There is one gap in the official image: it does not ship the QEMU guest agent. Without the agent, Proxmox cannot tell Terraform which IP the VM got, and apply waits until it times out. The script installs the agent into the image before it becomes a template:

virt-customize -q -a "$IMAGE" --install qemu-guest-agent --truncate /etc/machine-id

Truncating /etc/machine-id matters too. Without it, every clone shares the same machine ID, so they can end up with the same DHCP lease.

Run it once on the Proxmox host, as root:

sh create-template.sh # template 9000 on local-lvm
sh create-template.sh 9000 local-zfs vmbr0 # if Proxmox runs on ZFS

Step 2: an API token

pveum user token add root@pam terraform --privsep 0

Proxmox shows the value once. Keep it in environment variables, not in a file that might end up in Git:

export PROXMOX_VE_ENDPOINT=https://pve.example.lan:8006/
export PROXMOX_VE_API_TOKEN='root@pam!terraform=THE_VALUE'

Step 3: VMs as a list

All the VMs are entries in one map in terraform.tfvars:

vms = {
  app-1 = { ipv4_cidr = "192.168.10.31/24" }
  db-1 = { ipv4_cidr = "192.168.10.32/24", memory_mb = 4096, disk_gb = 40 }
}

To add a VM, add a line. To remove one, delete its line. Then:

terraform init && terraform apply
ssh ops@192.168.10.31

Three errors you will probably hit

  • x509: certificate signed by unknown authority. A new Proxmox install uses a self-signed certificate. Either issue a real one (Datacenter → ACME) or, on a network you trust, export PROXMOX_VE_INSECURE=true.
  • storage 'local-lvm' does not exist. If you installed Proxmox on ZFS, the storage is called local-zfs. Set datastore = "local-zfs".
  • An apply that failed halfway. Proxmox may keep VMs that Terraform no longer tracks. Delete them in the UI before running apply again, or their IDs and IPs will clash.

What comes next

At this point you have clean, reproducible VMs, and the next job is configuring what runs inside them. In my setup that is four Ansible roles, all driven by the inventory Terraform writes:

  • SSH hardening, firewall, fail2ban and automatic security updates. The SSH role checks that you can still log in before it disables passwords.
  • k3s, pinned to a version, with secrets encryption on.
  • Docker from the official repository, with log limits.
  • Encrypted nightly backups with restic.

Those roles, plus a step-by-step guide tested end to end, are in the paid version, Rackplinth. The template script and the Terraform module in this post are free, and you can use them in any project.

Questions, or a setup where this breaks? Tell me in the comments.

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.