Introduction
Terraform enables infrastructure as code (IaC) for Proxmox environments, allowing you to define, version, and automate VM provisioning. Instead of manually creating VMs through the Proxmox GUI, you can declare your desired infrastructure state in configuration files and let Terraform handle the deployment.
Benefits of using Terraform with Proxmox:
- Reproducibility: Identical infrastructure across dev, staging, and production
- Version Control: Track infrastructure changes in Git alongside application code
- Automation: Integrate with CI/CD pipelines for automated deployments
- Documentation: Configuration files serve as living documentation
- Efficiency: Provision multiple VMs simultaneously with minimal effort
Common use cases:
- Rapidly deploying test environments for development teams
- Provisioning identical staging environments that mirror production
- Automating homelab infrastructure setup
- Managing Kubernetes cluster nodes
- Creating disaster recovery infrastructure
Prerequisites
Before starting, ensure you have:
- Proxmox VE installed and accessible (tested with 6.x, 7.x, 8.x)
- VM template already created (see related post: “How to Create a VM Template in Proxmox”)
- Terraform installed on your local machine (v1.0+)
- Basic familiarity with Terraform concepts (providers, resources, variables)
- Administrative access to Proxmox for API token creation
Installation:
# macOS (using Homebrew)
brew install terraform
# Linux (using package manager)
# Ubuntu/Debian
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt update && sudo apt install terraform
# Verify installation
terraform version
# Should output: Terraform v1.x.x
Part 1: Create Proxmox API User and Token
Terraform needs API credentials to communicate with Proxmox. Follow these steps to create a dedicated API user with appropriate permissions.
Step 1: Create API User
Log into Proxmox web interface (https://your-proxmox-ip:8006)
Navigate to Datacenter → Permissions → Users
Click “Add” to create a new user
Configure the user:
- User name:
terraform-api - Realm:
Proxmox VE authentication server(pam) - Full user:
terraform-api@pam - Password: Leave blank (we’ll use API tokens instead)
- Groups: Optional, create “automation” group if desired
- User name:
Click “Add”
Step 2: Create API Token
Navigate to Datacenter → Permissions → API Tokens
Click “Add”
Configure the token:
- User:
terraform-api@pam(select from dropdown) - Token ID:
terraform(creates full ID:terraform-api@pam!terraform) - ⚠️ IMPORTANT: Uncheck “Privilege Separation” box
- This allows the token to use the user’s full permissions
- If checked, the token would need separate permission grants
- User:
Click “Add”
⚠️ CRITICAL: Save the token secret immediately
- The secret is displayed only once
- Copy both the Token ID and Secret to a secure location
- Example output:
Token ID: terraform-api@pam!terraform Secret: 12345678-abcd-1234-abcd-123456789abc
Step 3: Grant Permissions to API Token
Navigate to Datacenter → Permissions
Add API Token Permission:
- Click “Add” → “API Token Permission”
- Path:
/(root, applies to entire datacenter) - API Token:
terraform-api@pam!terraform(select from dropdown) - Role:
Administrator(or create custom role with minimum required permissions) - ☑ Check “Propagate” (applies permissions to all child objects)
- Click “Add”
Add User Permission (redundant but ensures proper access):
- Click “Add” → “User Permission”
- Path:
/ - User:
terraform-api@pam - Role:
Administrator - ☑ Check “Propagate”
- Click “Add”
Step 4: Verify Permissions
Test the API token using curl:
export PROXMOX_API_URL="https://your-proxmox-ip:8006/api2/json"
export PROXMOX_TOKEN_ID="terraform-api@pam!terraform"
export PROXMOX_SECRET="12345678-abcd-1234-abcd-123456789abc"
# Test API access
curl -k -H "Authorization: PVEAPIToken=${PROXMOX_TOKEN_ID}=${PROXMOX_SECRET}" \
"${PROXMOX_API_URL}/version"
Expected response:
{"data":{"version":"7.4","release":"1","repoid":"6f2af22e"}}
If you receive an authentication error, double-check that “Privilege Separation” was unchecked during token creation.
Part 2: Set Up Terraform Project
Now that API access is configured, let’s create the Terraform project structure.
Step 1: Create Project Directory
mkdir -p ~/terraform/proxmox
cd ~/terraform/proxmox
git init
Step 2: Create .gitignore
IMPORTANT: Never commit sensitive files (secrets, state files) to version control.
cat > .gitignore << 'EOF'
# Terraform state files (contain sensitive data)
*.tfstate
*.tfstate.*
*.tfstate.backup
# Terraform lock and modules
.terraform/
.terraform.lock.hcl
# Sensitive variable files
terraform.tfvars
*.auto.tfvars
# Crash logs
crash.log
# Override files
override.tf
override.tf.json
*_override.tf
*_override.tf.json
EOF
Step 3: Create providers.tf
This file defines the Terraform and Proxmox provider configuration.
cat > providers.tf << 'EOF'
terraform {
required_version = ">= 1.0"
required_providers {
proxmox = {
source = "telmate/proxmox"
version = "~> 3.0" # Use latest 3.x version
}
}
}
provider "proxmox" {
pm_api_url = var.proxmox_api_url
pm_api_token_id = var.pm_api_token_id
pm_api_token_secret = var.pm_api_token_secret
pm_tls_insecure = true # Set to false if using valid SSL certificate
# Optional: Enable debugging (comment out for production)
# pm_log_enable = true
# pm_log_file = "terraform-plugin-proxmox.log"
# pm_debug = true
# pm_log_levels = {
# _default = "debug"
# _capturelog = ""
# }
}
EOF
Key changes from older guides:
- ✅ Updated provider version from
2.9.14to~> 3.0(latest stable) - ✅ Added version constraints for better reproducibility
- ✅ Included optional debugging configuration
Step 4: Create variables.tf
Define input variables for configurable values:
cat > variables.tf << 'EOF'
variable "proxmox_api_url" {
description = "The URL of the Proxmox API (e.g., https://192.168.1.100:8006/api2/json)"
type = string
}
variable "pm_api_token_id" {
description = "API Token ID (e.g., terraform-api@pam!terraform)"
type = string
}
variable "pm_api_token_secret" {
description = "API Token Secret"
type = string
sensitive = true # Prevents secret from appearing in logs
}
variable "proxmox_node" {
description = "Proxmox node name (e.g., pve)"
type = string
default = "pve"
}
variable "template_name" {
description = "Name of the VM template to clone"
type = string
default = "ubuntu-22.04-template"
}
variable "vm_count" {
description = "Number of VMs to create"
type = number
default = 1
validation {
condition = var.vm_count > 0 && var.vm_count <= 10
error_message = "VM count must be between 1 and 10"
}
}
variable "vm_name_prefix" {
description = "Prefix for VM names"
type = string
default = "ubuntu-server"
}
variable "vm_cores" {
description = "Number of CPU cores per VM"
type = number
default = 2
}
variable "vm_memory" {
description = "Memory in MB per VM"
type = number
default = 2048
}
variable "vm_disk_size" {
description = "Disk size (e.g., 32G)"
type = string
default = "32G"
}
variable "network_subnet" {
description = "Network subnet for VMs (e.g., 192.168.1)"
type = string
default = "192.168.1"
}
variable "vm_ip_start" {
description = "Starting IP address (last octet)"
type = number
default = 100
}
variable "gateway_ip" {
description = "Gateway IP address"
type = string
}
variable "dns_servers" {
description = "DNS servers (comma-separated)"
type = string
default = "8.8.8.8,8.8.4.4"
}
EOF
Step 5: Create main.tf
Define the VM resources to be created:
cat > main.tf << 'EOF'
resource "proxmox_vm_qemu" "ubuntu_vm" {
count = var.vm_count
# VM Configuration
name = "${var.vm_name_prefix}-${count.index + 1}"
target_node = var.proxmox_node
vmid = 100 + count.index # VM IDs: 100, 101, 102, etc.
desc = "Ubuntu VM created by Terraform on ${timestamp()}"
# Clone from template
clone = var.template_name
full_clone = true
# Start VM automatically after creation
onboot = true
startup = ""
# Enable QEMU Guest Agent
agent = 1
# Hardware Configuration
cores = var.vm_cores
sockets = 1
cpu = "host"
memory = var.vm_memory
scsihw = "virtio-scsi-pci"
# Boot configuration
bootdisk = "scsi0"
boot = "order=scsi0"
# Disk configuration
disks {
scsi {
scsi0 {
disk {
size = var.vm_disk_size
storage = "local-lvm" # Change to your storage name
}
}
}
}
# Network configuration - Static IP
network {
model = "virtio"
bridge = "vmbr0"
}
ipconfig0 = "ip=${var.network_subnet}.${var.vm_ip_start + count.index}/24,gw=${var.gateway_ip}"
nameserver = var.dns_servers
# Cloud-init settings (if template has cloud-init)
# ciuser = "admin"
# cipassword = "changeme" # Don't hardcode passwords!
# sshkeys = file("~/.ssh/id_rsa.pub")
# Lifecycle management
lifecycle {
ignore_changes = [
network, # Ignore network changes (prevents unnecessary updates)
]
}
}
# Output the VM information
output "vm_details" {
description = "Details of created VMs"
value = {
for vm in proxmox_vm_qemu.ubuntu_vm :
vm.name => {
id = vm.vmid
ip_address = vm.ipconfig0
cores = vm.cores
memory = vm.memory
}
}
}
output "vm_ip_addresses" {
description = "IP addresses of created VMs"
value = [
for i in range(var.vm_count) :
"${var.network_subnet}.${var.vm_ip_start + i}"
]
}
EOF
⚠️ Important Notes:
storage = "local-lvm"- Change to match your Proxmox storage namebridge = "vmbr0"- Change if using a different network bridge- Disk configuration syntax is specific to provider version 3.x
Step 6: Create terraform.tfvars
⚠️ WARNING: This file contains secrets. Never commit to version control (it’s in .gitignore).
cat > terraform.tfvars << EOF
# Proxmox API Configuration
proxmox_api_url = "https://192.168.1.50:8006/api2/json"
pm_api_token_id = "terraform-api@pam!terraform"
pm_api_token_secret = "12345678-abcd-1234-abcd-123456789abc"
# Proxmox Node
proxmox_node = "pve"
# Template Configuration
template_name = "ubuntu-22.04-template"
# VM Configuration
vm_count = 3
vm_name_prefix = "web-server"
vm_cores = 2
vm_memory = 4096
vm_disk_size = "50G"
# Network Configuration
network_subnet = "192.168.1"
vm_ip_start = 110
gateway_ip = "192.168.1.1"
dns_servers = "8.8.8.8,8.8.4.4"
EOF
Replace with your actual values:
proxmox_api_url: Your Proxmox server IPpm_api_token_secret: The secret saved from Part 1gateway_ip: Your network gatewaynetwork_subnet: Your network’s first three octets
Part 3: Deploy VMs with Terraform
Now that configuration is complete, let’s provision the infrastructure.
Step 1: Initialize Terraform
terraform init
Expected output:
Initializing the backend...
Initializing provider plugins...
- Finding telmate/proxmox versions matching "~> 3.0"...
- Installing telmate/proxmox v3.0.1-rc1...
- Installed telmate/proxmox v3.0.1-rc1
Terraform has been successfully initialized!
This downloads the Proxmox provider plugin and prepares the working directory.
Step 2: Validate Configuration
terraform validate
Expected output:
Success! The configuration is valid.
If you see errors, check syntax in your .tf files.
Step 3: Plan Deployment
terraform plan
Example output:
Terraform will perform the following actions:
# proxmox_vm_qemu.ubuntu_vm[0] will be created
+ resource "proxmox_vm_qemu" "ubuntu_vm" {
+ name = "web-server-1"
+ vmid = 100
+ target_node = "pve"
+ clone = "ubuntu-22.04-template"
+ cores = 2
+ memory = 4096
...
}
# proxmox_vm_qemu.ubuntu_vm[1] will be created
...
# proxmox_vm_qemu.ubuntu_vm[2] will be created
...
Plan: 3 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ vm_details = {
+ web-server-1 = { id = 100, ip_address = "ip=192.168.1.110/24,gw=192.168.1.1", ... }
+ web-server-2 = { id = 101, ip_address = "ip=192.168.1.111/24,gw=192.168.1.1", ... }
+ web-server-3 = { id = 102, ip_address = "ip=192.168.1.112/24,gw=192.168.1.1", ... }
}
+ vm_ip_addresses = [
+ "192.168.1.110",
+ "192.168.1.111",
+ "192.168.1.112",
]
Review carefully:
- Verify VM names, IDs, and IP addresses are correct
- Check cores, memory, and disk sizes match expectations
- Ensure no unintended resources will be modified or destroyed
Step 4: Apply Configuration
terraform apply
Terraform will show the plan again and prompt for confirmation:
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
Type yes and press Enter.
Deployment progress:
proxmox_vm_qemu.ubuntu_vm[0]: Creating...
proxmox_vm_qemu.ubuntu_vm[1]: Creating...
proxmox_vm_qemu.ubuntu_vm[2]: Creating...
proxmox_vm_qemu.ubuntu_vm[0]: Still creating... [10s elapsed]
proxmox_vm_qemu.ubuntu_vm[1]: Still creating... [10s elapsed]
proxmox_vm_qemu.ubuntu_vm[2]: Still creating... [10s elapsed]
...
proxmox_vm_qemu.ubuntu_vm[0]: Creation complete after 45s [id=pve/qemu/100]
proxmox_vm_qemu.ubuntu_vm[1]: Creation complete after 47s [id=pve/qemu/101]
proxmox_vm_qemu.ubuntu_vm[2]: Creation complete after 49s [id=pve/qemu/102]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Outputs:
vm_details = {
"web-server-1" = { id = 100, ip_address = "ip=192.168.1.110/24,gw=192.168.1.1", cores = 2, memory = 4096 }
"web-server-2" = { id = 101, ip_address = "ip=192.168.1.111/24,gw=192.168.1.1", cores = 2, memory = 4096 }
"web-server-3" = { id = 102, ip_address = "ip=192.168.1.112/24,gw=192.168.1.1", cores = 2, memory = 4096 }
}
vm_ip_addresses = [
"192.168.1.110",
"192.168.1.111",
"192.168.1.112",
]
Step 5: Verify VMs in Proxmox
- Check Proxmox GUI: You should see three new VMs (100, 101, 102) running
- Test SSH access (if cloud-init configured):
ssh [email protected]
ssh [email protected]
ssh [email protected]
Part 4: Manage Infrastructure
View Current State
# Show current infrastructure
terraform show
# Show output values
terraform output
# Show specific output
terraform output vm_ip_addresses
Modify Infrastructure
To add more VMs, update terraform.tfvars:
# Change from 3 to 5 VMs
vm_count = 5
Then apply changes:
terraform plan
terraform apply
Terraform will add 2 more VMs (IDs 103, 104) without touching existing ones.
Destroy Infrastructure
⚠️ WARNING: This will permanently delete all VMs managed by this Terraform state.
# Preview what will be destroyed
terraform plan -destroy
# Destroy all resources
terraform destroy
Confirm by typing yes when prompted.
Example output:
proxmox_vm_qemu.ubuntu_vm[2]: Destroying... [id=pve/qemu/102]
proxmox_vm_qemu.ubuntu_vm[1]: Destroying... [id=pve/qemu/101]
proxmox_vm_qemu.ubuntu_vm[0]: Destroying... [id=pve/qemu/100]
...
Destroy complete! Resources: 3 destroyed.
Troubleshooting
Error: “401 Unauthorized”
Cause: API token authentication failed
Solutions:
- Verify token ID and secret in
terraform.tfvars - Ensure “Privilege Separation” was unchecked during token creation
- Check permissions were granted (see Part 1, Step 3)
- Test token with curl (see Part 1, Step 4)
Error: “Template ‘ubuntu-22.04-template’ not found”
Cause: Template doesn’t exist or name is incorrect
Solutions:
# List available templates (on Proxmox host)
pvesh get /cluster/resources --type vm | grep template
# Update template_name in terraform.tfvars to match exact name
Error: “Resource already exists” (VM ID conflict)
Cause: VM with that ID already exists
Solutions:
- Change
vmidvalues inmain.tfto unused IDs - Remove conflicting VMs in Proxmox GUI
- Import existing VM into Terraform state:
terraform import proxmox_vm_qemu.ubuntu_vm[0] pve/qemu/100
Error: “Storage ’local-lvm’ not found”
Cause: Storage name doesn’t match Proxmox configuration
Solutions:
# List available storage (on Proxmox host)
pvesh get /storage
# Update storage value in main.tf to match
Error: “Network bridge ‘vmbr0’ not found”
Cause: Network bridge doesn’t exist
Solutions:
# Check available bridges (on Proxmox host)
ip link show | grep vmbr
# Update bridge in main.tf to match
VMs Created but Won’t Start
Cause: Template may not be bootable or hardware incompatibility
Solutions:
- Verify template boots correctly by cloning it manually
- Check Proxmox logs:
/var/log/pve/tasks/ - Ensure template has
qemu-guest-agentinstalled - Verify CPU type compatibility (
cpu = "host"vscpu = "kvm64")
Best Practices
Security
Protect Sensitive Files:
# Verify .gitignore is protecting secrets
git status
# Should NOT show:
# - terraform.tfvars
# - *.tfstate
# - .terraform/
Use Environment Variables (alternative to terraform.tfvars):
export TF_VAR_proxmox_api_url="https://192.168.1.50:8006/api2/json"
export TF_VAR_pm_api_token_id="terraform-api@pam!terraform"
export TF_VAR_pm_api_token_secret="secret-here"
# Run terraform without terraform.tfvars
terraform plan
Rotate API Tokens Regularly:
- Create new token every 90 days
- Delete old tokens after migration
- Document token rotation in runbooks
State Management
Use Remote State for team environments:
# Add to providers.tf
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "proxmox/terraform.tfstate"
region = "us-east-1"
}
}
Or use Terraform Cloud for free remote state.
Back Up State Files:
# State files contain sensitive data and resource mappings
cp terraform.tfstate terraform.tfstate.backup-$(date +%Y%m%d)
Code Organization
Split into Modules for larger deployments:
terraform/proxmox/
├── modules/
│ ├── vm/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── network/
│ ├── main.tf
│ └── variables.tf
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ └── terraform.tfvars
│ └── prod/
│ ├── main.tf
│ └── terraform.tfvars
└── README.md
Use Workspaces for environment separation:
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
terraform workspace select dev
terraform apply
Advanced Examples
Example 1: VMs with Different Specifications
# main.tf
locals {
vms = {
web = {
count = 2
cores = 2
memory = 4096
disk = "50G"
ip_start = 110
}
db = {
count = 1
cores = 4
memory = 8192
disk = "100G"
ip_start = 120
}
cache = {
count = 2
cores = 1
memory = 2048
disk = "20G"
ip_start = 130
}
}
}
resource "proxmox_vm_qemu" "servers" {
for_each = {
for vm_config in flatten([
for vm_type, config in local.vms : [
for i in range(config.count) : {
key = "${vm_type}-${i + 1}"
type = vm_type
index = i
config = config
}
]
]) : vm_config.key => vm_config
}
name = each.key
target_node = var.proxmox_node
vmid = 100 + index(keys({ for k, v in local.vms : k => v }), each.value.type) * 10 + each.value.index
clone = var.template_name
full_clone = true
cores = each.value.config.cores
memory = each.value.config.memory
# ... rest of configuration
}
Example 2: Integration with cloud-init
# variables.tf
variable "ssh_public_key" {
description = "SSH public key for VM access"
type = string
default = ""
}
# main.tf
resource "proxmox_vm_qemu" "ubuntu_vm" {
# ... existing configuration ...
# Cloud-init configuration
ciuser = "admin"
sshkeys = var.ssh_public_key != "" ? var.ssh_public_key : file("~/.ssh/id_rsa.pub")
# Optional: Run commands on first boot
# cicustom = "user=local:snippets/user-data.yml"
}
Example 3: Data sources for dynamic configuration
# Query existing VMs
data "proxmox_virtual_environment_vms" "existing" {
node_name = var.proxmox_node
}
# Use in logic
locals {
next_vmid = max(data.proxmox_virtual_environment_vms.existing.vms[*].vm_id...) + 1
}
Next Steps
Now that you have automated VM provisioning:
- Integrate with Ansible for configuration management
- Set up CI/CD pipeline for infrastructure deployments
- Explore Terraform modules for reusable components
- Implement remote state for team collaboration
- Add monitoring and alerting for provisioned VMs
Related Guides
- How to Create a VM Template in Proxmox
- How to Create a Cloud-Init Template VM in Proxmox
- How to Manage Infrastructure with Terraform and Ansible
Additional Resources
Last updated: November 2025