Vagrant Terraform Provider

This is an experimental Vagrant plugin for Proxmox that uses Terraform under the hood to operate the virtual machines.

Could be extended to support other Terraform providers in addition to Proxmox but at the moment I'm not planning to.

Things I'm not planning to do (due to lack of time and resources):

  • Support other Terraform providers unless I have to move away from Proxmox to something else.
  • Add support for suspend / snapshots.
  • Support / test anything other than Ubuntu/Fedora
  • Support Ruby 2.x

Installation

From rubygems.org

vagrant plugin install vagrant-terraform

Build & install:

$ gem build *.gemspec
$ vagrant plugin install *.gem

Usage

Prerequisites

Requirements

Tested on Ubuntu 22.04 and Fedora 40.

Terraform

Configuration

Create API user https://registry.terraform.io/providers/Telmate/proxmox/latest/docs#creating-the-proxmox-user-and-role-for-terraform

pveum role add Terraform -privs "Datastore.AllocateSpace Datastore.AllocateTemplate Datastore.Audit Pool.Allocate Sys.Audit Sys.Console Sys.Modify VM.Allocate VM.Audit VM.Clone VM.Config.CDROM VM.Config.Cloudinit VM.Config.CPU VM.Config.Disk VM.Config.HWType VM.Config.Memory VM.Config.Network VM.Config.Options VM.Migrate VM.Monitor VM.PowerMgmt SDN.Use"

pveum user add terraform@pve
pveum aclmod / -user terraform@pve -role Terraform
pveum user token add terraform@pve automation --privsep 0

The value on the last line is your API token secret you are going to need in your Vagrantfile.

Privilege separation needs to be disabled from API key due to: https://github.com/Telmate/terraform-provider-proxmox/issues/784

Prepare a VM template on your Proxmox PVE

Download cloud image

wget https://cloud-images.ubuntu.com/noble/current/SHA256SUMS https://cloud-images.ubuntu.com/noble/current/SHA256SUMS.gpg https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
gpg --keyid-format long --verify SHA256SUMS.gpg SHA256SUMS  # Requires public key "UEC Image Automatic Signing Key <cdimage@ubuntu.com>"
sha256sum -c --ignore-missing SHA256SUMS

Customize it the way you want with virt-customize

export LIBGUESTFS_BACKEND=direct; virt-customize -a noble-server-cloudimg-amd64.img --install qemu-guest-agent --run-command "truncate -s 0 /etc/machine-id /var/lib/dbus/machine-id"

Create a VM and convert it to template

qm create 9001 --memory 1024 --net0 virtio,bridge=vmbr0 --scsihw virtio-scsi-pci --name ubuntu-noble-template --machine q35,viommu=virtio --ostype l26 --rng0 /dev/urandom
qm set 9001 --virtio0 [STORAGE_NAME]:0,import-from=/root/noble-server-cloudimg-amd64.img
qm set 9001 --ide2 [STORAGE_NAME]:cloudinit --boot c --bootdisk virtio0
qm template 9001

Supported Commands

  1. vagrant up
  2. vagrant destroy
  3. vagrant ssh [-c 'command']
  4. vagrant ssh-config
  5. vagrant halt
  6. vagrant reload
  7. vagrant status

Simple configuration example with two network interfaces

Vagrant.configure("2") do |config|
  config.vm.box = 'dummy'
  config.vm.box_url = 'https://github.com/mika-b/vagrant-terraform/blob/main/example_box/dummy.box?raw=true'

  config.vm.hostname = "example-vm.local"

  config.vm.synced_folder './', '/vagrant', type: 'rsync', rsync__exclude: ["log/", ".git/"]  # , rsync__verbose: false
  config.ssh.forward_agent = true

  config.vm.network :private_network,
    :terraform__network_name => 'testmgmt' # DHCP
  config.vm.network :private_network,
    :terraform__network_name => 'vmbr0', :terraform__ip => '192.168.0.51/24', :terraform__gateway => '192.168.0.1'

  config.vm.provider :terraform do |terraform|
    terraform.api_url = 'https://[PVE_ADDRESS]:8006/api2/json'
    terraform.api_token_id = "terraform@pve!automation"
    terraform.api_token_secret = "[API_TOKEN]"
    terraform.insecure = true
    terraform.debug = false
    terraform.description = "Created by: #{ENV['USER']}"
    terraform.template = 'ubuntu-noble-template'
    terraform.cpu_cores = 2
    terraform.memory_size = '4 GiB'
    terraform.disk_size = '15 GB'
    terraform.target_node = 'pve1'
    terraform.storage_domain = '[STORAGE_NAME]'
    terraform.nameserver = '1.1.1.1 1.0.0.1'
    terraform.searchdomain = 'example.com'
  end

  config.vm.provision "shell", inline: <<-SHELL
    ip a
SHELL
end