Use Terraform or OpenTofu to create a VM in Proxmox

Creating virtual machines in Proxmox with Terraform or OpenTofu, relying on official cloud images for a lightweight Infrastructure as Code approach.

Following an article about using Terraform to create LXCs for Proxmox , I want to dig deeper into the topic by covering virtual machine creation this time.

Prerequisites: prior knowledge of Terraform, administrative rights on Proxmox, and the ability to perform operations without major constraints. I also use OpenTofu , which is compatible with Terraform without any code changes at the time of writing.

Previously, I created virtual machine templates with Packer . However, depending on yet another tool no longer suits me and tends to weigh down the process. From now on, I use the “cloud” images created and provided by vendors such as Debian, Ubuntu, or even Arch Linux, Fedora, etc. The advantage of this kind of image is its lightness and integrity. In return, you’ll have to test your creations a bit more, since the images come with very few packages and pre-defined configuration by default.

The working environment for this article is as follows:

  • a Proxmox VE 9.x server
  • a code editor
  • Terraform 1.12.x or OpenTofu 1.12.x

Preparing the working environment#

Before starting the automated deployment with Terraform or OpenTofu, make sure your environment is properly configured by following these steps:

  1. Installing Terraform or OpenTofu: In a terminal, install Terraform by following the instructions in the official HashiCorp documentation , adapted to your operating system. Likewise, for OpenTofu, refer to this documentation link to get all the information you need. Make sure Terraform or OpenTofu is correctly installed by checking the version: $ terraform version.

Access to Proxmox: 1. Make sure you have access to your Proxmox host via a web browser. 2. Also check access via SSH for command-line operations. 3. Credentials for Proxmox: have the credentials of the “root” user or another user with elevated privileges on Proxmox to create virtual machines and manage storage. You can use “API Tokens” (read the beginning of this page to learn more).

With these prerequisites in place, you’re ready to automate the deployment of virtual machines with Terraform or OpenTofu.

Preparing Proxmox#

To simplify operations in your test environments and shorten this document, you can use the root@pam account created during the Proxmox installation. This account has all privileges. For finer management of rights and access, creating a dedicated user and its token is recommended.

The complete Terraform code#

Finally, here is the complete code I use to create VMs on Proxmox with Terraform or OpenTofu. The code is fairly complete; I like to have as much information and as many attributes as possible to define my resources as precisely as possible.

The “variables.tf” file:

bash
### variables.tf

# The DNS domain used for cloud-init configuration.
variable "cloudinit_dns_domain" {
  type        = string
  default     = "home.arpa"
  description = "The DNS domain to be configured by cloud-init."
}

# A list of DNS servers used for cloud-init configuration.
variable "cloudinit_dns_servers" {
  type        = list(string)
  description = "A list of DNS server addresses for cloud-init configuration. Provide the IP addresses as a list of strings, e.g., [\"9.9.9.9\", \"149.112.112.112\"] (without backslashes)."
}

# SSH keys to be added to the authorized_keys file during cloud-init setup.
variable "cloudinit_ssh_keys" {
  type        = list(string)
  description = "SSH public keys to be included in the user's authorized_keys file."
}

# The username and password for the initial user account set up by cloud-init.
variable "cloudinit_user_account" {
  type        = string
  description = "Username for the initial user account created by cloud-init."
}

# ID of the datastore where the VM will be stored.
variable "vm_datastore_id" {
  type        = string
  description = "ID of the datastore to store the virtual machine disks."
}

# Format for the VM disk file (e.g., qcow2, vmdk).
variable "vm_disk_file_format" {
  type        = string
  description = "File format of the virtual machine's disk image."
}

# Name or identifier for the node.
variable "node_name" {
  type        = string
  description = "Name or identifier of the Proxmox node."
}

# API token for authenticating with the Proxmox Virtual Environment (PVE) host.
variable "pve_api_token" {
  type        = string
  description = "API token used to authenticate with the PVE server. Used too into a PVE cluster"
}

# SSH user on the Proxmox node, used by the provider to upload files over SSH.
variable "pve_api_user" {
  type        = string
  description = "SSH username used by the provider on the Proxmox node (commonly root)."
}

# IP address or hostname of the PVE host.
variable "pve_host_address" {
  type        = string
  description = "IP address or hostname of a PVE host."
}

variable "tags" {
  type        = list(string)
  description = "A list of tags to label the VM."
}

# Directory path for temporary files during VM setup.
variable "tmp_dir" {
  type        = string
  description = "Path to a directory used for storing temporary files like ISO uploads."
}

# Network bridge interface for connecting the VM's LAN.
variable "vm_bridge_lan" {
  type        = string
  description = "Network bridge name for the virtual machine's LAN connection."
}

# Number of CPU cores assigned to the VM.
variable "vm_cpu_cores_number" {
  type        = number
  description = "Number of CPU cores allocated to the virtual machine."
}

# Type of CPUs used in the VM (e.g., host, kvm).
variable "vm_cpu_type" {
  type        = string
  description = "Type of CPU model assigned to the virtual machine."
}

# Description for the VM.
variable "vm_description" {
  type        = string
  description = "Human-readable description or notes about the virtual machine. Markdown compatible."
}

# Size of the VM's disk in GB.
variable "vm_disk_size" {
  type        = number
  description = "Size of the disk for the virtual machine, specified in gigabytes. E.g. If you want a 2 Gb disk, type '1863'."
}

variable "vm_gateway_ipv4" {
  description = "The gateway IP address for the VM's network interface"
  type        = string
}

# Unique identifier for the VM instance (e.g., ID).
variable "vm_id" {
  type        = number
  description = "Unique numerical identifier for the virtual machine. Limited to 9 digits"
}

variable "vm_ipv4_address" {
  description = "The IPv4 address assigned to the VM, without its network mask."
  type        = string
}

# Maximum amount of memory allocated to the VM in MB.
variable "vm_memory_max" {
  type        = number
  description = "Maximum RAM allocation for the virtual machine, specified in megabytes. E.g. for 4 Gb, type '4096'."
}

# Minimum amount of memory allocated to the VM in MB.
variable "vm_memory_min" {
  type        = number
  description = "Minimum RAM allocation for the virtual machine, specified in megabytes. E.g. for 4 Gb, type '4096'."
}

# Name or label for identifying the VM.
variable "vm_name" {
  type        = string
  description = "Name used to identify the virtual machine."
}

# Order to start the VM
variable "vm_startup_order" {
  type        = string
  description = "A number to set the order of the VM starting."
}

# Number of CPU sockets allocated to the VM.
variable "vm_socket_number" {
  type        = number
  description = "Number of CPU sockets assigned to the virtual machine. Don't assign more virtual sockets than you have physically."
}

The “variables.auto.tfvars” file — this is the file you need to edit so you can have a container that matches your needs:

bash
### variables.auto.tfvars
cloudinit_dns_domain   = "your.domain.net"
cloudinit_dns_servers  = ["9.9.9.9"]
cloudinit_ssh_keys     = ["ssh-ed25519 changeme"]
cloudinit_user_account = "jho"
node_name              = "pve"
pve_api_token          = "terrabot@pve!token_name=token_secret"
pve_api_user           = "root"
pve_host_address       = "https://pve:8006"
tags                   = ["linux", "opentofu"]
tmp_dir                = "/tmp"
vm_bridge_lan          = "vmbr0"
vm_cpu_cores_number    = 2
vm_cpu_type            = "x86-64-v2-AES"
vm_datastore_id        = "local"
vm_description         = "Managed by terraform."
vm_disk_file_format    = "raw"
vm_disk_size           = 64
vm_gateway_ipv4        = "172.16.0.254"
vm_id                  = 9993
vm_ipv4_address        = "172.16.241.10"
vm_memory_max          = 8192
vm_memory_min          = 4096
vm_name                = "tf_example"
vm_startup_order       = "1"
vm_socket_number       = 1

The “vm.tf” file:

I use cloud-init to make configuring the machine’s network easier. Eventually, this configuration will also allow applying configurations via YAML-format files.

hcl
### vm.tf
terraform {
  required_providers {
    proxmox = {
      source  = "bpg/proxmox"
      version = "~> 0.107"
    }
    random = {
      source = "hashicorp/random"
      version = "3.7.2"
    }
  }
}

provider "proxmox" {
  endpoint  = var.pve_host_address
  api_token = var.pve_api_token
  insecure  = true
  ssh {
    agent    = true
    username = var.pve_api_user
  }
  tmp_dir = var.tmp_dir
}

# cloud-image Ubuntu 24 avec cloud-init
resource "proxmox_virtual_environment_download_file" "ubuntu24_cloudimg_20260518" {
  checksum           = "53fdde898feed8b027d94baa9cfe8229867f330a1d9c49dc7d84465ee7f229f7"
  checksum_algorithm = "sha256"
  content_type       = "iso"
  datastore_id       = var.vm_datastore_id
  file_name          = "ubuntu-24.04-server-20260518-cloudimg-amd64.img"
  node_name          = var.node_name
  upload_timeout     = 360
  url                = "https://cloud-images.ubuntu.com/noble/20260518/noble-server-cloudimg-amd64.img"
}

resource "proxmox_virtual_environment_vm" "vm1_example" {
  depends_on = [proxmox_virtual_environment_download_file.ubuntu24_cloudimg_20260518]

  description         = var.vm_description
  keyboard_layout     = "fr"
  machine             = "q35"
  migrate             = true
  name                = var.vm_name
  node_name           = var.node_name
  scsi_hardware       = "virtio-scsi-single"
  started             = true
  stop_on_destroy     = true # Force stop when destroying
  tablet_device       = false # VM without GUI
  tags                = var.tags
  timeout_create      = 180
  timeout_shutdown_vm = 30
  timeout_stop_vm     = 30
  vm_id               = var.vm_id

  agent {
    enabled = true
    timeout = "5m"
    trim    = true
  }

  cpu {
    cores   = var.vm_cpu_cores_number
    flags   = []
    numa    = true
    sockets = var.vm_socket_number
    type    = var.vm_cpu_type
  }

  disk {
    aio          = "native"
    cache        = "none"
    datastore_id = var.vm_datastore_id
    discard      = "on"
    file_format  = var.vm_disk_file_format
    file_id      = "${proxmox_virtual_environment_download_file.ubuntu24_cloudimg_20260518.datastore_id}:iso/${proxmox_virtual_environment_download_file.ubuntu24_cloudimg_20260518.file_name}"
    interface    = "scsi0"
    iothread     = true
    replicate    = false
    size         = var.vm_disk_size
  }

  efi_disk {
    datastore_id      = var.vm_datastore_id
    pre_enrolled_keys = false
    type              = "4m"
  }

  initialization {
    datastore_id = var.vm_datastore_id
    dns {
      domain  = var.cloudinit_dns_domain
      servers = var.cloudinit_dns_servers
    }
    ip_config {
      ipv4 {
        address = "${var.vm_ipv4_address}/16"
        gateway = var.vm_gateway_ipv4
      }
    }
    user_account {
      keys     = var.cloudinit_ssh_keys
      username = var.cloudinit_user_account
    }
  }

  memory {
    dedicated = var.vm_memory_max
    floating  = var.vm_memory_min
  }

  network_device {
    bridge = var.vm_bridge_lan
    model  = "virtio"
  }

  operating_system {
    type = "l26"
  }

  startup {
    order      = var.vm_startup_order
    up_delay   = 15
    down_delay = 30
  }

  serial_device {}

  tpm_state {
    datastore_id = var.vm_datastore_id
    version      = "v2.0"
  }

  vga {
    type = "virtio"
  }
}

In these files, particularly “variables.auto.tfvars”, the values need to be changed and must match your needs. Read each file carefully and adapt them to your infrastructure. I have maximized the use of variables to make adjustments easier.

Also keep in mind that creating a virtual machine with Terraform/OpenTofu can be destructive when updating the parameters in the .tf files. If you update the "proxmox_virtual_environment_download_file" "ubuntu24_cloudimg_20260518" resource, the VM will be destroyed and rebuilt with the new file. So remember to back up your data as much as possible.

Related articles

#tuto No image
#linux

Create Proxmox LXC containers with Terraform/OpenTofu

These days, task automation and the creation of immutable infrastructures are essential. With this in mind, the use of tools such as Terraform or its open-source fork OpenTofu becomes particularly interesting for the creation of LXC, automated and in mass.

Read more
#tuto No image
#linux

Proxmox, system update impossible

Your Proxmox server won't update because of a conflicting package? The solution is at the bottom of this page...

Read more
#tuto No image
#linux

su or sudo ?

My notes and some information about su and sudo, to gain privileged access on a Linux system.

Read more

Latest in #linux