How to Deploy Your First Proxmox Virtual Machine Using Terraform

Sumit SurSumit Sur
6 min read

🧠 Why Terraform for Proxmox?

While Proxmox has a great web UI, infrastructure-as-code lets you:

  • Automate repeatable VM deployments

  • Keep configurations under version control

  • Easily spin up multi-VM Setups

  • Reduce human error

👋 Quick heads-up!

This guide is Part 2 of a multi-part series.

In this article, we’ll walk through How to Deploy a Virtual Machine in proxmox Using Terraform

👉 Jump to Part 1: How to Set Up Proxmox with Terraform

📦 Prerequisites

Before you begin, make sure you have:

✅ A running Proxmox VE host or cluster
✅ A user with API access (e.g., terraform-user@pve)
✅ A cloud-init template VM ready to be cloned
✅ Terraform installed on your machine
✅ Installed the Telmate Proxmox Terraform provider

🗂 Project Directory Structure

Here’s the structure of the deployment repo we’ll use:

proxmox-vm-deploy/
├── main.tf
├── provider.tf
├── variables.tf
├── terraform.tfvars

Let’s go through each file.

🧩 provider.tf – Connect Terraform to Proxmox

terraform {
  required_providers {
    proxmox = {
      source = "Telmate/proxmox"
      version = "3.0.2-rc01" # use the latest version available
    }
  }
}


provider "proxmox" {
  pm_api_url = var.proxmox_api_url # the variable is defined in terraform.tfvars.This should match the URL in your Proxmox web interface, typically something like "https://<proxmox-ip>:8006/api2/json"
  pm_parallel = 1
  pm_debug = false
  pm_tls_insecure = true
}

This sets up the connection to your Proxmox host. Make sure:

  • The user exists in Proxmox (pveum user add terraform-user@pve)

  • A suitable role (e.g., Terraform_Provisioner) with VM permissions is assigned

  • load the API tokens for connecting to proxmox as environment variables

👉 Check out part-1 of the series for a step-by-step guide on creating the required users, roles and tokens to connect via terraform

📜 variables.tf – Input Configuration


variable "vm_name" {
  type = string
}

variable "clone" {
  type = string
}

variable "ipconfig0" {
  type = string
}

variable "vmid" {
  type = number
}

variable "memory" {
  type = number
}

variable "cores" {
  type = number
}

variable "disk_size" {
  type = string
  description = "The size of the disk, should be at least as big as the disk in the template"
  default = "20G"

}

variable "storage" {
  type = string
  description = "the storage where the VM disk will be created"

}

variable "ssh-public-key" {
    type = string
    description = "SSH public key for the VMs"
    sensitive = true

}

variable "proxmox_api_url" {
    type        = string
    description = "Proxmox API URL"
}

variable "target_node" {
    type        = string
    description = "The Proxmox node where the VM will be created"

}

variable "nameserver" {
    type        = string
    description = "Nameserver for the VM"
    default     = "1.1.1.1 8.8.8.8"

}

variable "cicustom" {
    type        = string
    description = "Cloud-Init custom configuration"
    default     = "vendor=local:snippets/qemu-guest-agent.yml"

}

variable "cipassword" {
    type        = string
    description = "Cloud-Init password for the VM"
    sensitive = true

}

These variables define how your VM will look—name, clone template, IP config, memory, CPU, etc.

⚙️ terraform.tfvars – Your Custom Values

ssh-public-key = "ssh-ed25519 AAAAC3NzaC1lZXXXXXXXXXXXXXXXXXXXXXXwSOCiZ/OkpPDR3bR2tK4STIm+gnJk"
target_node = "proxmox-server-IP" # The Proxmox node where the VM will be created
cicustom = "value=local:snippets/install-packages.yml" # /var/lib/vz/snippets/install-packages.yml #
cipassword = "ubuntu"
ipconfig0 = "ip=dhcp"
vmid = 1000 # optional, if not set, Proxmox will assign a random VMID
vm_name = "ubuntu-vm"
clone = "ubuntu-24-04-cloudinit-copy" # The template to clone from
cores = 4 # Number of CPU cores
memory = 4096 # Memory in MB
nameserver = "1.1.1.1 8.8.8.8"
disk_size = "20G" # The size of the disk, should be at least as big as the disk in the template
storage = "hdd-vm-data" # The storage where the VM disk will be created

This is your configuration layer—values you want to pass to variables. Keep this file out of version control (.gitignore) if it includes sensitive info.

🏗 main.tf – Create the Virtual Machine

#create a new VM from a template with cloud-init enabled
resource "proxmox_vm_qemu" "ubuntu-vm" {

  # Basic VM configuration
  vmid        = var.vmid
  name        = var.vm_name
  target_node = var.target_node # The node where the VM will be created
  agent       = 1 # Enable the QEMU guest agent
  cpu {
    cores = var.cores
    sockets = 1
    numa = true
    type = "x86-64-v2-AES"
  }
  memory      = var.memory # Memory in MB
  bios        = "ovmf" # Use OVMF for UEFI support
  boot        = "order=scsi0" # has to be the same as the OS disk of the template
  clone       = var.clone # The template to clone from
  scsihw      = "virtio-scsi-single" # Use VirtIO SCSI controller
  vm_state    = "running" # "running" or "stopped"
  automatic_reboot = true

  # Cloud-Init configuration
  cicustom   = var.cicustom
  ciupgrade  = true # it will upgrade the OS to the latest version
  nameserver = var.nameserver
  ipconfig0  = var.ipconfig0
  skip_ipv6  = true
  ciuser     = "root" # The user to use for the cloud-init script
  cipassword = var.cipassword # Password for the cloud-init user
  sshkeys    = var.ssh-public-key # The SSH public key to be added to the VM

  # Most cloud-init images require a serial device for their display
  serial {
    id = 0
  }

  # EFI disk for UEFI boot
  # This is required for cloud-init images that use UEFI
  # If your template does not use UEFI, you can remove this block
  efidisk {
    efitype = "4m" 
    storage = "hdd-vm-data"
  }

  # Disk configuration
  disks {
    scsi {
      scsi0 {
        # We have to specify the disk from our template, else Terraform will think it's not supposed to be there
        disk {
          storage = var.storage
          # The size of the disk should be at least as big as the disk in the template. If it's smaller, the disk will be recreated
          size    = var.disk_size
        }
      }

  # Some images require a cloud-init disk on the IDE controller, others on the SCSI or SATA controller
      scsi1 {
        cloudinit {
          storage = "hdd-vm-data"
        }
      }
    }
  }

  network {
    id = 0
    bridge = "vmbr0"
    model  = "virtio"
  }
}

Here’s what it does:

  • Clones an existing cloud-init-enabled VM template

  • Assigns VM name, IP, memory, CPU, etc.

  • Injects cloud-init config, such as SSH key and default user

  • Enables QEMU guest agent to enhance functionality

▶️ How to Run It

Open a terminal inside the project directory and follow these steps:

# use single quotes for the API token ID because of the exclamation mark
export PM_API_TOKEN_ID='terraform-user@pve!tf_token'
export PM_API_TOKEN_SECRET="XXXXXX-XXXX-XXXXX-XXXX-XXXXXXXXXXX"

# Initialize Terraform
terraform init


# Review execution plan
terraform plan

# Apply the configuration
terraform apply

✅ Verify in Proxmox

  • Go to your Proxmox Web UI

  • You’ll see a the VM

  • Confirm network and SSH access

  • Check if the VM booted from your template and has your custom config


🛠 Troubleshooting Tips

  • SSH not working? Ensure cloud-init was enabled in your template and your public SSH key is valid.

  • Error: Permission denied? Double-check the Proxmox user's role and permissions.

  • Wrong IP? Validate your ipconfig0 syntax (should follow ip=x.x.x.x/xx,gw=x.x.x.x).


🧪 What’s Next?

Once you get one VM working, you can:

  • Create multiple VMs using for_each

  • Automate post-deploy scripts via null_resource and remote-exec

  • Turn this into a Kubernetes cluster or homelab infra stack!

0
Subscribe to my newsletter

Read articles from Sumit Sur directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sumit Sur
Sumit Sur