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

  1. Log into Proxmox web interface (https://your-proxmox-ip:8006)

  2. Navigate to Datacenter → Permissions → Users

  3. Click “Add” to create a new user

  1. 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
  2. Click “Add”

Step 2: Create API Token

  1. Navigate to Datacenter → Permissions → API Tokens

  2. Click “Add”

  1. 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
  2. Click “Add”

  3. ⚠️ 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

  1. Navigate to Datacenter → Permissions

  2. 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”
  3. 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.14 to ~> 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 name
  • bridge = "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 IP
  • pm_api_token_secret: The secret saved from Part 1
  • gateway_ip: Your network gateway
  • network_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

  1. Check Proxmox GUI: You should see three new VMs (100, 101, 102) running
  2. Test SSH access (if cloud-init configured):

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:

  1. Verify token ID and secret in terraform.tfvars
  2. Ensure “Privilege Separation” was unchecked during token creation
  3. Check permissions were granted (see Part 1, Step 3)
  4. 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:

  1. Change vmid values in main.tf to unused IDs
  2. Remove conflicting VMs in Proxmox GUI
  3. 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:

  1. Verify template boots correctly by cloning it manually
  2. Check Proxmox logs: /var/log/pve/tasks/
  3. Ensure template has qemu-guest-agent installed
  4. Verify CPU type compatibility (cpu = "host" vs cpu = "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:

  1. Integrate with Ansible for configuration management
  2. Set up CI/CD pipeline for infrastructure deployments
  3. Explore Terraform modules for reusable components
  4. Implement remote state for team collaboration
  5. Add monitoring and alerting for provisioned VMs

  • 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