Terraform Create a Proxmox Debian VM

John Wu

2026/05/31

Tags: tech guides

Table of Contents

Goals

The goals of this guide is to:

  1. Set up a user that terraform can use to access and modify proxmox
  2. Set up terraform so that it can use the Proxmox user
  3. 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:

  1. Upload snippets
  2. Import disks via source_file.path (local file transfer to node)
  3. 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.

  1. Private variables like API tokens or usernames
  2. 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:

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