Skip to main content
RunBook Academy

Proxmox VEXX · CLI & AutomationAPI automation

Terraform provider for Proxmox: infrastructure as code for VMs and containers

Intermediate⏱ ~22 min🧪 Lab requiredterraform

What you'll learn

  • Set up the Telmate/proxmox Terraform provider
  • Define VMs and containers as code with version-controlled configuration
  • Use terraform plan and apply to roll out changes safely
  • Integrate with state backends for team collaboration

Prerequisites

Verified against Proxmox VE 9.2.4 · Proxmox Backup Server 4.2.5 · Ceph Squid / Tentacle · Debian 13 (Trixie) · Linux kernel 7.0 (PVE 9.2 default) · 2026-08-07

Not yet marked complete on this device.

Terraform provider for Proxmox: infrastructure as code for VMs and containers

Terraform gives you version-controlled, repeatable infrastructure. The Telmate Proxmox provider lets you declare PVE VMs and containers in HCL, plan changes before applying, and roll back when something goes wrong.

Installing Terraform and the provider

# Install Terraform
wget https://releases.hashicorp.com/terraform/1.7.0/terraform_1.7.0_linux_amd64.zip
unzip terraform_1.7.0_linux_amd64.zip
mv terraform /usr/local/bin/

terraform version
# Terraform v1.7.0

The provider is automatically downloaded on terraform init from the Terraform registry. Configure it via:

# versions.tf
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    proxmox = {
      source  = "Telmate/proxmox"
      version = ">= 0.65.0"
    }
  }
}

# Provider config — uses API token
provider "proxmox" {
  endpoint = var.proxmox_api_url
  api_token = var.proxmox_api_token
  insecure = true   # Set to false in production with proper certs
}

variable "proxmox_api_url" {
  type = string
}

variable "proxmox_api_token" {
  type      = string
  sensitive = true
}

For the API token, create one with the right privileges:

# On the PVE host
pvesh create /access/users/terraform@pve --comment "Terraform automation"
pvesh create /access/roles/TerraformRole \
  --privs "VM.Allocate,VM.Config.Disk,VM.Config.CPU,VM.Config.Memory,VM.Config.Network,VM.Config.Options,VM.PowerMgmt,Datastore.Allocate,Datastore.Audit"
pvesh create /access/acl --path / --roles TerraformRole --users terraform@pve
pvesh create /access/users/terraform@pve/token/automation --privsep 0

Set the token:

export TF_VAR_proxmox_api_url="https://pve-01.cluster.example.com:8006/api2/json"
export TF_VAR_proxmox_api_token="terraform@pve!automation=12345678-..."

Defining a VM

# variables.tf
variable "image_storage" {
  default = "local-zfs"
}

variable "iso_storage" {
  default = "local-zfs"
}

# main.tf
resource "proxmox_vm_qemu" "web_server" {
  name        = "web-01"
  target_node = "pve-01"
  vmid        = 100

  # Hardware
  cores   = 2
  sockets = 1
  memory  = 2048
  scsihw  = "virtio-scsi-single"

  # Disk
  disk {
    size     = "32G"
    storage  = var.image_storage
    type     = "scsi"
    iothread = true
    ssd      = true
  }

  # Network
  network {
    model  = "virtio"
    bridge = "vmbr0"
  }

  # Cloud-init
  ipconfig0 = "ip=dhcp"

  # Boot
  boot    = "order=scsi0"
  agent   = 1
  onboot  = true

  # OS-specific
  ostype = "l26"

  # Lifecycle
  lifecycle {
    ignore_changes = [
      # Don't fight with cloud-init for these
      ciuser,
      sshkeys,
    ]
  }
}

output "web_server_ip" {
  value = proxmox_vm_qemu.web_server.default_ipv4_address
}

Run the standard Terraform workflow:

terraform init       # Download providers
terraform validate   # Syntax check
terraform plan       # Show what would change
terraform apply      # Apply changes

Using variables and modules

Terraform modules let you parameterise VM definitions:

# modules/web-vm/main.tf
variable "name" {}
variable "vmid" {}
variable "target_node" {}
variable "memory" { default = 2048 }
variable "cores" { default = 2 }
variable "disk_size" { default = "32G" }

resource "proxmox_vm_qemu" "this" {
  name        = var.name
  target_node = var.target_node
  vmid        = var.vmid
  cores       = var.cores
  memory      = var.memory

  # ... rest of config
}

# root main.tf
module "web_vms" {
  source   = "./modules/web-vm"
  for_each = toset(["web-01", "web-02", "web-03"])

  name        = each.value
  vmid        = 200 + index(["web-01", "web-02", "web-03"], each.value)
  target_node = "pve-01"
}

This creates three VMs from one module definition.

State management

Terraform state tracks what was created. Local state is fine for single-developer projects. For teams, use a remote backend:

# S3 backend (recommended)
terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "proxmox/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

The DynamoDB table provides state locking so two engineers don’t apply conflicting changes simultaneously.

Common workflows

Reproducible dev environments

# Develop locally
terraform plan -out=tfplan
# Review the plan
terraform show tfplan
# Apply
terraform apply tfplan
# Tear down when done
terraform destroy

Bulk VM creation from a list

locals {
  vms = {
    "db-master"  = { cores = 4, memory = 8192, disk = 100 }
    "db-replica" = { cores = 4, memory = 8192, disk = 100 }
    "cache-01"   = { cores = 2, memory = 4096, disk = 20 }
    "cache-02"   = { cores = 2, memory = 4096, disk = 20 }
  }
}

resource "proxmox_vm_qemu" "batch" {
  for_each = local.vms

  name        = each.key
  target_node = "pve-01"
  vmid        = 300 + index(sort(keys(local.vms)), each.key)
  cores       = each.value.cores
  memory      = each.value.memory

  disk {
    size    = "${each.value.disk}G"
    storage = var.image_storage
  }

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

  ostype = "l26"
  agent  = 1
}

Drift detection

Terraform detects drift between declared state and actual state:

# Run plan regularly (e.g., daily via CI)
terraform plan -detailed-exitcode
# Exit code 2 = drift detected

A daily plan in CI tells you when something has drifted from the declared state — usually because someone made a change via the GUI.

Common mistakes

  • State in version control. The .tfstate file should NEVER be in git. It contains secrets and dynamic data.
  • Long apply without plan review. Always review the plan carefully. Terraform’s diff is the only safety net.
  • Hardcoded secrets. Use environment variables or a secret manager. Terraform variables marked sensitive = true are still in the state file.
  • Drift via GUI. Once Terraform owns a resource, manual GUI changes create drift. Either use Terraform for everything or document the GUI escape hatch.

Production considerations

  • State file encryption. Use an encrypted backend (S3 with SSE, Azure Storage with CMK) so state — which may contain secrets — isn’t readable by anyone with bucket access.
  • Lock contention. With multiple engineers, state locking prevents conflicting changes. Use a backend that supports locking (S3+DynamoDB, Terraform Cloud, etc.).
  • Module registry. For team-scale Terraform, host modules in a private registry. Reuse instead of copy-paste.
  • Drift as a signal. Daily terraform plan in CI is a free configuration audit.

Key takeaways

  • Terraform + Telmate provider for declarative PVE management.
  • API tokens with least privilege.
  • Always terraform plan before terraform apply.
  • Remote state backend with locking for teams.

Knowledge check

Knowledge check · 4 questions

  1. Q1. What is the standard Telmate provider source for Terraform?

  2. Q2. Terraform state holds secrets in clear text, so it belongs in a remote backend rather than in version control.

  3. Q3. Which of these are Terraform best practices? (Select all that apply)

  4. Q4. Name the Terraform command that shows what would change without applying.

Passing score: 75%. Answers are checked in this browser.