Skip to content

GingerGraham/terraform-bgp-proxmox-vm-cloudimage

Repository files navigation

proxmox_vm_linux_cloudinit

Reusable Terraform module for provisioning cloud-init VMs on Proxmox VE with the bpg/proxmox provider.

Public module usage

This module is designed to be consumed as a standalone public module:

  • No dependency on private/local helper modules
  • All deployment-specific settings are exposed as input variables
  • Provider configuration is expected in the caller (root module)

Minimal example

terraform {
  required_version = ">= 1.0"

  required_providers {
    proxmox = {
      source  = "bpg/proxmox"
      version = "~> 0.97.1"
    }
  }
}

provider "proxmox" {
  endpoint  = var.proxmox_endpoint
  api_token = var.proxmox_api_token
  insecure  = false
}

module "vm" {
  source = "<org>/proxmox-vm-linux-cloudinit/proxmox"

  vm_name       = "app-01"
  cluster_nodes = ["pve1", "pve2"]

  cloud_image_reference = {
    node_name    = "pve1"
    datastore_id = "local"
    content_type = "import"
    file_name    = "debian-12-genericcloud-amd64.qcow2"
  }

  cloud_init_mode = "builtin"
  cloud_init_builtin = {
    datastore_id   = "local"
    distro_profile = "debian"
    username       = "ops"
    ssh_keys       = ["ssh-ed25519 AAAA..."]
    timezone       = "UTC"
    dns_domain     = "example.internal"
  }

  disk_datastore = "local-lvm"
  network_bridge = "vmbr0"

  efi_disk = {
    datastore_id = "local-lvm"
  }
}

Cloud-init modes

  • builtin: module renders cloud-init from structured inputs (cloud_init_builtin)
  • custom: caller passes rendered cloud-init payloads (cloud_init_custom)
  • native: uses Proxmox native cloud-init fields only (cloud_init_native)

Built-in distro profiles

Supported cloud_init_builtin.distro_profile values:

  • generic
  • debian
  • ubuntu
  • fedora
  • rhel
  • centos_stream
  • rocky
  • almalinux
  • opensuse
  • arch

Chrony handling in built-in mode is profile-specific:

  • Fedora/RHEL-family/OpenSUSE profiles write /etc/chrony.d/makestep.conf
  • Debian/Ubuntu profiles write /etc/chrony/conf.d/makestep.conf
  • generic and arch profiles add makestep 1.0 -1 to /etc/chrony.conf if missing

Migration from Earlier Versions

Important: This module has been updated to support bpg/proxmox provider version 0.97.1 with expanded VM configuration options.

See MIGRATION-GUIDE.md for:

  • List of variables with changed defaults
  • Migration strategies for existing deployments
  • Details on the new strict_provider_defaults compatibility toggle

Quick summary: By default, legacy module defaults are preserved (strict_provider_defaults = false). No immediate action is required for existing deployments, but explicitly setting values is recommended for production workloads.

Advanced Features

This module now supports the full feature set of the proxmox_virtual_environment_vm resource, including:

  • Advanced CPU options: architecture, flags, hotplug, NUMA, affinity
  • Advanced memory options: shared memory, hugepages
  • Advanced disk options: AIO mode, cache policy, speed limits, backup control
  • Additional disks: attach multiple disks with per-disk configuration
  • Additional network devices: multi-NIC VMs with VLAN tagging and rate limiting
  • Hardware passthrough: PCIe devices, USB devices, virtiofs mappings
  • Security: TPM state device, AMD SEV encryption
  • System devices: audio, VGA, watchdog, RNG
  • NUMA topology: explicit NUMA node configuration
  • Lifecycle control: startup order, reboot behavior, protection flags
  • Timeouts: per-operation timeout customization

Refer to the inputs table below for the complete list of available options.

Disk interface expectations and behavior

The module supports a primary disk (disk_interface) and optional additional disks (additional_disks). Disk interfaces are handled with the following rules.

  • Primary disk accepts scsi, sata, or virtio with optional index.
    • Bus-only values are normalized to index 0.
    • Examples: scsi -> scsi0, virtio -> virtio0, sata2 stays sata2.
  • Additional disks accept ide, sata, scsi, or virtio with optional index.
    • interface may be omitted (or set to empty string) for auto-assignment.
    • Bus-only values are normalized to index 0 (for example virtio -> virtio0).
    • datastore_id is optional; when omitted, the disk inherits disk_datastore.

Auto-assignment for additional disks

When an additional disk omits interface, the module auto-assigns the next available index on the same bus family as the primary disk interface.

  • Primary disk virtio0 with additional disks omitting interface will assign virtio1, virtio2, and so on, skipping any already-explicitly-used indexes.
  • If no free index is available on that bus, planning fails with a clear error.

Validation and safety checks

Before apply, the module enforces:

  • Every resolved disk interface must match ideN, sataN, scsiN, or virtioN.
  • Interface names must be unique across primary and additional disks.
  • Auto-assignment must have enough free indexes for all disks with omitted interfaces.

Datastore behavior for additional disks

  • Primary disk always uses disk_datastore.
  • Additional disks use additional_disks[*].datastore_id when provided.
  • If additional_disks[*].datastore_id is omitted, it defaults to disk_datastore.

Example: mixed explicit and auto-assigned additional disks

module "vm" {
  # ...

  disk_interface = "virtio0"

  additional_disks = [
    {
      size = 50
      # interface omitted -> auto-assigned (virtio1)
    },
    {
      interface = "virtio3"
      size      = 100
    },
    {
      size = 200
      # interface omitted -> auto-assigned (virtio2)
    }
  ]
}

Recommended consumer patterns

Use these defaults as a safe baseline for most deployments.

  • Set the primary disk explicitly to index 0 (for example virtio0 or scsi0).
  • Prefer omitting additional_disks[*].interface unless you need deterministic device naming.
  • Keep all additional disks on one bus family per VM unless you have a specific hardware/guest requirement.
  • Set disk_datastore explicitly and only override per-disk datastore_id when required.
  • Use explicit disk sizes and avoid relying on implied defaults in production.

Recommended baseline (copy/paste)

module "vm" {
  # ...

  disk_datastore = "local-lvm"
  disk_interface = "virtio0"
  disk_size_gb   = 40
  disk_discard   = "on"
  disk_iothread  = true
  disk_ssd       = true

  additional_disks = [
    {
      size = 100
    },
    {
      size = 200
    }
  ]
}

When to set explicit additional disk interfaces

Set explicit additional_disks[*].interface only when you need stable, predefined device addresses for guest-level automation, monitoring, or strict OS mount mapping expectations.

Common anti-patterns to avoid

  • Setting primary disk to a non-zero index without a clear reason.
  • Manually assigning additional disk indexes that collide with primary or other additional disks.
  • Mixing auto-assigned and manually assigned interfaces without checking index gaps and intent.
  • Using bus-only values expecting non-zero indexes (for example virtio always normalizes to virtio0).

Documentation workflow (terraform-docs)

This repository uses terraform-docs to keep variable and output references accurate.

  • Config file: .terraform-docs.yml
  • Injection markers are present near the end of this README

Preferred commands from repository root:

make docs
make docs-check

Equivalent script commands:

./scripts/terraform-docs-generate.sh
./scripts/terraform-docs-check.sh

Underlying Podman invocation:

podman run --rm \
  -v "$PWD:/terraform-docs:Z" \
  -w /terraform-docs \
  quay.io/terraform-docs/terraform-docs:latest \
  markdown table --config /terraform-docs/.terraform-docs.yml .

Terraform Reference

Requirements

Name Version
terraform >= 1.0
proxmox ~> 0.97.1

Providers

Name Version
proxmox ~> 0.97.1
random n/a

Modules

No modules.

Resources

Name Type
proxmox_virtual_environment_file.builtin_user_data resource
proxmox_virtual_environment_file.meta_data resource
proxmox_virtual_environment_file.network_data resource
proxmox_virtual_environment_file.user_data resource
proxmox_virtual_environment_file.vendor_data resource
proxmox_virtual_environment_vm.vm resource
random_shuffle.node_selection resource

Inputs

Name Description Type Default Required
acpi Whether to enable ACPI bool true no
additional_disks Additional disk blocks
list(object({
interface = optional(string)
datastore_id = optional(string)
file_id = optional(string)
import_from = optional(string)
path_in_datastore = optional(string)
size = optional(number)
aio = optional(string)
backup = optional(bool)
cache = optional(string)
discard = optional(string)
file_format = optional(string)
iothread = optional(bool)
replicate = optional(bool)
serial = optional(string)
ssd = optional(bool)
speed = optional(object({
iops_read = optional(number)
iops_read_burstable = optional(number)
iops_write = optional(number)
iops_write_burstable = optional(number)
read = optional(number)
read_burstable = optional(number)
write = optional(number)
write_burstable = optional(number)
}))
}))
[] no
additional_network_devices Additional network_device blocks
list(object({
bridge = optional(string)
disconnected = optional(bool)
enabled = optional(bool)
firewall = optional(bool)
mac_address = optional(string)
model = optional(string)
mtu = optional(number)
queues = optional(number)
rate_limit = optional(number)
vlan_id = optional(number)
trunks = optional(string)
}))
[] no
agent_enabled Enable QEMU guest agent bool null no
agent_timeout How long to wait for QEMU guest agent during create/update string null no
agent_trim Enable QEMU agent FSTRIM bool false no
agent_type QEMU agent interface type string "virtio" no
agent_wait_for_ipv4 Wait for at least one non-link-local IPv4 address from guest agent bool false no
agent_wait_for_ipv6 Wait for at least one non-link-local IPv6 address from guest agent bool false no
amd_sev Optional AMD SEV configuration
object({
type = optional(string)
allow_smt = optional(bool)
kernel_hashes = optional(bool)
no_debug = optional(bool)
no_key_sharing = optional(bool)
})
null no
assigned_memory Assigned memory in MB number null no
audio_device Optional audio device configuration
object({
enabled = optional(bool)
device = optional(string)
driver = optional(string)
})
null no
auto_dhcp_on_builtin_custom Automatically set IPv4 DHCP in Proxmox ip_config for builtin mode and for custom mode when network_data is not supplied bool true no
bios_type BIOS type (seabios or ovmf) string null no
boot_order Optional explicit boot order list (for example ["scsi0", "ide2"]) list(string) null no
cdrom Optional CD-ROM configuration
object({
file_id = optional(string)
interface = optional(string, "ide3")
})
null no
clone Optional clone configuration
object({
vm_id = number
full = optional(bool)
datastore_id = optional(string)
node_name = optional(string)
retries = optional(number)
})
null no
cloud_image_lookup_enabled Whether to resolve cloud image by filename on each plan/apply. Set to false after initial create to avoid lookup failures when image files are later removed from datastore. bool true no
cloud_image_reference Reference to cloud image file for cloning
object({
node_name = string # Node where the image is stored
datastore_id = string # Datastore where image is located
content_type = string # Usually "import"
file_name = string # Filename of the cloud image
})
n/a yes
cloud_init_builtin Built-in standard cloud-init configuration rendered by the module. Use with cloud_init_mode = 'builtin'.
object({
datastore_id = string # Where to store the snippet file
username = string # Primary user to create
password = optional(string, "") # User password (empty = no password auth)
ssh_keys = optional(list(string), [])
timezone = optional(string, "UTC")
dns_domain = optional(string, "local")
lock_passwd = optional(bool, false)
distro_profile = optional(string, "generic")
user_shell = optional(string, "/bin/bash")
sudo_groups = optional(list(string))
packages = optional(list(string), [])
extra_packages = optional(list(string), [])
enable_firewall = optional(bool, false)
manage_qemu_guest_agent = optional(bool)
runcmd = optional(list(string), [])
extra_runcmd = optional(list(string), [])
write_files = optional(list(object({
path = string
content = string
owner = optional(string, "root:root")
permissions = optional(string, "0644")
})), [])
extra_write_files = optional(list(object({
path = string
content = string
owner = optional(string, "root:root")
permissions = optional(string, "0644")
})), [])
})
null no
cloud_init_custom Custom cloud-init data files
object({
user_data = optional(string) # Cloud-config YAML content
meta_data = optional(string) # Meta-data content
network_data = optional(string) # Network configuration
vendor_data = optional(string) # Vendor-specific data
datastore_id = string # Where to store snippet files
})
null no
cloud_init_interface Interface for cloud-init drive (defaults based on BIOS type: sata0 for OVMF, ide2 for SeaBIOS) string null no
cloud_init_mode Cloud-init mode: 'native' for Proxmox native, 'custom' for caller-rendered YAML, 'builtin' for module standard template string "custom" no
cloud_init_native Native Proxmox cloud-init configuration
object({
user_name = optional(string)
user_password = optional(string)
ssh_keys = optional(list(string), [])
dns_domain = optional(string)
dns_servers = optional(list(string), [])
ipv4_address = optional(string, "dhcp")
ipv4_gateway = optional(string)
ipv6_address = optional(string)
ipv6_gateway = optional(string)
})
null no
cluster_nodes List of cluster nodes for random selection (required if node_name is null) list(string) [] no
cpu_additional Additional CPU options
object({
architecture = optional(string)
flags = optional(list(string), [])
hotplugged = optional(number)
limit = optional(number)
numa = optional(bool)
sockets = optional(number)
units = optional(number)
affinity = optional(string)
})
null no
cpu_cores Number of CPU cores number null no
cpu_type CPU type string null no
delete_unreferenced_disks_on_destroy Delete unreferenced disks on destroy bool true no
description VM description string "" no
disk_advanced Advanced settings for the primary disk
object({
aio = optional(string)
backup = optional(bool)
cache = optional(string)
file_format = optional(string)
replicate = optional(bool)
serial = optional(string)
speed = optional(object({
iops_read = optional(number)
iops_read_burstable = optional(number)
iops_write = optional(number)
iops_write_burstable = optional(number)
read = optional(number)
read_burstable = optional(number)
write = optional(number)
write_burstable = optional(number)
}))
})
null no
disk_datastore Datastore for VM disks string n/a yes
disk_discard Enable discard/TRIM string null no
disk_interface Primary disk interface (indexed form like scsi0/virtio1, or bus-only scsi/sata/virtio which normalizes to index 0) string "scsi0" no
disk_iothread Enable iothread bool null no
disk_size_gb Disk size in GB number 20 no
disk_ssd Mark disk as SSD bool null no
efi_disk EFI disk configuration for OVMF BIOS
object({
datastore_id = string
file_format = optional(string, "raw")
type = optional(string, "4m")
pre_enrolled_keys = optional(bool, false)
})
null no
hook_script_file_id Optional hook script file ID string null no
hostname Hostname for cloud-init (defaults to vm_name if not specified) string null no
hostpci Optional host PCI passthrough mappings
list(object({
device = string
id = optional(string)
mapping = optional(string)
mdev = optional(string)
pcie = optional(bool)
rombar = optional(bool)
rom_file = optional(string)
xvga = optional(bool)
}))
[] no
hotplug Hotplug feature string (for example 'network,disk,usb', '1', or '0') string null no
initialization_advanced Advanced initialization block controls
object({
datastore_id = optional(string)
interface = optional(string)
file_format = optional(string)
})
null no
keyboard_layout Keyboard layout string "en-us" no
kvm_arguments Arbitrary arguments passed to KVM string null no
machine VM machine type string "pc" no
memory_additional Additional memory options
object({
shared = optional(number)
hugepages = optional(string)
keep_hugepages = optional(bool)
})
null no
migrate Allow VM migration bool null no
minimum_memory Minimum memory in MB (defaults to assigned_memory when null) number null no
network_advanced Advanced settings for the primary network device
object({
disconnected = optional(bool)
enabled = optional(bool)
firewall = optional(bool)
mac_address = optional(string)
mtu = optional(number)
queues = optional(number)
rate_limit = optional(number)
vlan_id = optional(number)
trunks = optional(string)
})
null no
network_bridge Network bridge string n/a yes
network_model Network device model string "virtio" no
node_name Proxmox node to deploy to (null for random selection from cluster) string null no
numa Optional NUMA topology configuration
list(object({
device = string
cpus = string
memory = number
hostnodes = optional(string)
policy = optional(string)
}))
[] no
on_boot Start VM on host boot bool null no
os_type Operating system type string "l26" no
pool_id Optional Proxmox pool ID to assign the VM string null no
protection Sets the protection flag for the VM and disables the remove VM and remove disk operations bool false no
purge_on_destroy Purge VM from backup jobs on destroy bool true no
reboot Reboot VM after initial creation bool false no
reboot_after_update Reboot VM after update if needed bool true no
rng Optional random number generator configuration
object({
source = string
max_bytes = optional(number)
period = optional(number)
})
null no
scsi_hardware SCSI hardware type string "virtio-scsi-pci" no
serial_devices Serial devices. Empty list keeps a single default socket serial device for backwards compatibility.
list(object({
device = optional(string)
}))
[] no
smbios Optional SMBIOS type1 configuration
object({
family = optional(string)
manufacturer = optional(string)
product = optional(string)
serial = optional(string)
sku = optional(string)
uuid = optional(string)
version = optional(string)
})
null no
started Whether to start VM bool true no
startup Optional startup/shutdown behavior
object({
order = number
up_delay = optional(number)
down_delay = optional(number)
})
null no
stop_on_destroy Stop VM on destroy instead of shutdown bool null no
strict_provider_defaults When true, unset module inputs resolve to documented bpg/proxmox provider defaults instead of legacy module defaults bool false no
tablet_device Enable USB tablet device bool true no
tags VM tags list(string) [] no
tags_environment Environment tags merged before tags list(string) [] no
tags_global Global tags merged before tags list(string)
[
"server",
"linux"
]
no
tags_instance Instance-specific tags merged before tags list(string) [] no
tags_role Role tags merged before tags list(string) [] no
template Whether to create as VM template bool false no
timeout_clone Timeout for cloning VM in seconds number 1800 no
timeout_create Timeout for creating VM in seconds number 1800 no
timeout_migrate Timeout for migrating VM in seconds number 1800 no
timeout_reboot Timeout for rebooting VM in seconds number 1800 no
timeout_shutdown_vm Timeout for shutting down VM in seconds number 1800 no
timeout_start_vm Timeout for starting VM in seconds number 1800 no
timeout_stop_vm Timeout for stopping VM in seconds number 300 no
tpm_state Optional TPM state device
object({
datastore_id = optional(string)
version = optional(string)
})
null no
usb Optional USB passthrough mappings
list(object({
host = optional(string)
mapping = optional(string)
usb3 = optional(bool)
}))
[] no
vga Optional VGA configuration
object({
type = optional(string)
memory = optional(number)
clipboard = optional(string)
})
null no
virtiofs Optional virtiofs mappings
list(object({
mapping = string
cache = optional(string)
direct_io = optional(bool)
expose_acl = optional(bool)
expose_xattr = optional(bool)
}))
[] no
vm_id VM ID (null for auto-assignment) number null no
vm_name Name of the VM in Proxmox (will be used as default hostname if hostname not specified) string n/a yes
watchdog Optional watchdog configuration
object({
enabled = optional(bool)
model = optional(string)
action = optional(string)
})
null no

Outputs

Name Description
ipv4_addresses IPv4 addresses of the VM
ipv6_addresses IPv6 addresses of the VM
mac_addresses MAC addresses of the VM
node_name The Proxmox node where the VM is deployed
vm_id The ID of the created VM
vm_name The name of the created VM

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Languages