Proxmox VEXX · CLI & AutomationAPI automation
Terraform provider for Proxmox: infrastructure as code for VMs and containers
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
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
.tfstatefile 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 = trueare 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 planin CI is a free configuration audit.
Key takeaways
- Terraform + Telmate provider for declarative PVE management.
- API tokens with least privilege.
- Always
terraform planbeforeterraform apply. - Remote state backend with locking for teams.
Knowledge check
Knowledge check · 4 questions
Q1. What is the standard Telmate provider source for Terraform?
Q2. Terraform state holds secrets in clear text, so it belongs in a remote backend rather than in version control.
Q3. Which of these are Terraform best practices? (Select all that apply)
Q4. Name the Terraform command that shows what would change without applying.
Passing score: 75%. Answers are checked in this browser.