Goals
The goals of this guide is to:
- Set up a user that terraform can use to access and modify proxmox
- Set up terraform so that it can use the Proxmox user
- Create a Debian VM using terraform
For the Debian VM, I will take the approach of using a Debian 13 (Trixie) generic cloud image and use Cloud Init to configure it. Later on I will write an article of how I used the Debian netinst image to create a “golden image,” but I’ve found the effort for that is not worth it compared to using the Debian cloud image.
User Setup
You’ll need to create a new user with a new token:
pveum user add terraform@pve
pveum role add Terraform -privs "Realm.AllocateUser, VM.PowerMgmt, VM.GuestAgent.Unrestricted, Sys.Console, Sys.Audit, Sys.AccessNetwork, VM.Config.Cloudinit, VM.Replicate, Pool.Allocate, SDN.Audit, Realm.Allocate, SDN.Use, Mapping.Modify, VM.Config.Memory, VM.GuestAgent.FileSystemMgmt, VM.Allocate, SDN.Allocate, VM.Console, VM.Clone, VM.Backup, Datastore.AllocateTemplate, VM.Snapshot, VM.Config.Network, Sys.Incoming, Sys.Modify, VM.Snapshot.Rollback, VM.Config.Disk, Datastore.Allocate, VM.Config.CPU, VM.Config.CDROM, Group.Allocate, Datastore.Audit, VM.Migrate, VM.GuestAgent.FileWrite, Mapping.Use, Datastore.AllocateSpace, Sys.Syslog, VM.Config.Options, Pool.Audit, User.Modify, VM.Config.HWType, VM.Audit, Sys.PowerMgmt, VM.GuestAgent.Audit, Mapping.Audit, VM.GuestAgent.FileRead, Permissions.Modify"
pveum aclmod / -user terraform@pve -role Terraform
pveum user token add terraform@pve provider --privsep=0
Make sure you copy the token value, as it will not be displayed again.
SSH Setup
If you would like to:
- Upload snippets
- Import disks via source_file.path (local file transfer to node)
- Configure idmap entries for LXCs
Then you’ll want to set up ssh.
useradd -m terraform
visudo -f /etc/sudoers.d/terraform
In visudo, add
terraform ALL=(root) NOPASSWD: /usr/sbin/pvesm
terraform ALL=(root) NOPASSWD: /usr/sbin/qm
terraform ALL=(root) NOPASSWD: /usr/bin/tee /var/lib/vz/snippets/[a-zA-Z0-9_][a-zA-Z0-9_.-]*
terraform ALL=(root) NOPASSWD: /usr/bin/sed -i * /etc/pve/lxc/*.conf
terraform ALL=(root) NOPASSWD: /usr/bin/tee -a /etc/pve/lxc/*.conf
If you’re not using the default local datastore for snippetes, change:
--terraform ALL=(root) NOPASSWD: /usr/bin/tee /var/lib/vz/snippets/[a-zA-Z0-9_][a-zA-Z0-9_.-]*
++terraform ALL=(root) NOPASSWD: /usr/bin/tee /mnt/pve/cephfs/snippets/[a-zA-Z0-9_][a-zA-Z0-9_.-]*
Make sure to change the path after /usr/bin/tee.
Finally you’ll need to copy your public key to the user under ~/.ssh/authorized_keys like normal.
Terraform Setup
Provider Section
terraform {
required_version = ">=1.5.0"
required_providers {
proxmox = {
source = "bpg/proxmox"
version = ">=0.99.0"
}
}
}
provider "proxmox" {
endpoint = "https://127.0.0.1:8006"
api_token = "aaaa-bbbb-cccc-dddd"
insecure = true
ssh {
agent = true
username = "terraform" # required when using api_token
private_key = file("~/.ssh/terraform")
node {
name = "pve"
address = "127.0.0.1"
port = 2200
}
}
}
Variables Section
There will be 2 types of variables and you might want to split them into different files.
- Private variables like API tokens or usernames
- Public variables like the datastore_id or something like that
I have the *.tfvars split into 2 files and the variables.tf remain the same because they don’t contain the values.
Under variables, you should define which variables will be used and the .tfvars file will have the definition.
# variables.tf
variable "pve_api_token" { # Generated from the Proxmox UI under Datacenter>Permissions>API Tokens
type = string
sensitive = true
}
variable "pve_api_url" {
type = string
sensitive = true
}
variable "pve_insecure" { # Whether you have HTTPS enabled or not (with a signed certificate)
type = bool
}
variable "virtual_environment_node_name" {
type = string
}
variable "datastore_id" { # The disk volume on your node, default is local-lvm
type = string
}
# variables.tfvars
pve_api_url = "https://x.x.x.x:8006/api2/json"
pve_api_token = "terraform@pve!provider=xxxx-xxxx-xxxx-xxxx"
pve_insecure = false
pve_insecure = "false"
virtual_environment_node_name = "pve"
datastore_id = "local-zfs"
At this point you can terraform validate and terraform plan to see if it connects.
VM Creation and Cloud Init Setup
For this section, you should be referencing these docs. Their docs are spread out everywhere so here’s a collection of them, the rest you can google it out:
- guide for cloned VM
- guide for cloud init
- guide I used for downloading VM image
- entire examples directory under the bgp proxmox provider repo
- all options for proxmox_virtual_environment_vm
Cloud Image
This is will be how we download the cloud image:
resource "proxmox_virtual_environment_download_file" "debian_cloud_image" {
content_type = "iso"
datastore_id = "vm-isos"
node_name = var.virtual_environment_node_name
url = "https://cloud.debian.org/images/cloud/trixie/20260402-2435/debian-13-genericcloud-amd64-20260402-2435.qcow2" # var.debian_cloudimg_url
file_name = "debian-13-genericcloud-amd64.img"
}
Cloud-init Snippet creation
This is where we’ll store a proxmox snippet that you should enable on the datastore in the UI first. The snippet will be used for cloud-init, modify at least the user ssh key.
resource "proxmox_virtual_environment_file" "user_data_cloud_config" {
content_type = "snippets"
datastore_id = "vm-isos"
node_name = var.virtual_environment_node_name
source_raw {
data = <<-EOF
#cloud-config
hostname: debian
timezone: Asia/Taipei
package_update: true
package_upgrade: true
apt:
conf: |
APT::Install-Recommends "0";
APT::Install-Suggests "0";
packages:
- openssh-server
- qemu-guest-agent
- sudo
- curl
- iproute2
- systemd-resolved
- vim
- python3
users:
- default
- name: provision
gecos: Provisioning User
primary_group: provision
groups: users
lock_passwd: false
passwd: $6$rounds=500000$Y77uf0rTuvPIh.W0$2M6SRrpYeUEtKpXIkDzkSquhMSdNpOG/rtyW/u516J5XMhzr2ybbwLxhpUJiSMSh/J42hCHaAJgQzYXUGWc8g/
ssh_authorized_keys:
- INSERT YOUR OWN KEY HERE: contents of ~/.ssh/id_rsa.pub
sudo: "ALL=(ALL) NOPASSWD:ALL"
ssh_pwauth: false
ssh_deletekeys: true
disable_root: true
disable_root_opts: no-port-forwarding,no-agent-forwarding,no-X11-forwarding
ssh_genkeytypes: ed25519
write_files:
- path: /etc/ssh/sshd_config.d/00-cloud-init-hardening.conf
owner: root:root
permissions: "0644"
content: |
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
runcmd:
- systemctl enable qemu-guest-agent
- systemctl start qemu-guest-agent
- echo "done" > /tmp/cloud-config.done
EOF
file_name = "user-data-cloud-config.yaml"
}
}
VM Template Creation
This section will create a template for future VMs to be based on.
resource "proxmox_virtual_environment_vm" "debian_template" {
name = "debian-template"
node_name = var.virtual_environment_node_name
vm_id = 9002
agent {
enabled = true
}
template = true
started = false
machine = "pc"
bios = "seabios"
description = "Managed by Terraform"
cpu {
cores = 2
}
memory {
dedicated = 2048
}
disk {
datastore_id = var.datastore_id
file_id = proxmox_virtual_environment_download_file.debian_cloud_image.id
interface = "scsi0"
iothread = true
discard = "on"
size = 20
}
initialization {
datastore_id = var.datastore_id
ip_config {
ipv4 {
address = "dhcp"
}
}
user_data_file_id = proxmox_virtual_environment_file.user_data_cloud_config.id
}
network_device {
bridge = "vmbr0"
model = "virtio"
vlan_id = 20
}
}
Defining The Actual VM
This will define an actual VM, the steps above were just setting this part up.
resource "proxmox_virtual_environment_vm" "debian_clone" {
name = "debian-clone"
node_name = var.virtual_environment_node_name
clone {
vm_id = proxmox_virtual_environment_vm.debian_template.id
datastore_id = var.datastore_id
full = true
}
agent {
enabled = true
wait_for_ip {
ipv4 = true
}
}
memory {
dedicated = 768
}
scsi_hardware = "virtio-scsi-single"
initialization {
datastore_id = "local-zfs"
user_data_file_id = proxmox_virtual_environment_file.user_data_cloud_config.id
dns {
servers = ["9.9.9.9"]
}
ip_config {
ipv4 {
address = "dhcp"
}
}
}
}
output "vm_ipv4_address" {
value = proxmox_virtual_environment_vm.debian_clone.ipv4_addresses[1][0]
}
Run:
terraform apply
Now you should see it in your Proxmox UI that your environment has been defined.
>> Home