La puissance des outils comme Terraform et OpenTofu résident dans leur capacité de mise à l'échelle. Créer une machine virtuelle est assez rapide, mais créons plusieurs machines pour exploiter au mieux l'outil.
J'utilise OpenTofu depuis début 2025 sur différentes infrastructures Proxmox, en ligne et déconnectées. Les exemples de code ci-dessous vous donneront une première base de travail. Je reprends la base des fichiers .tf selon l'article "Utiliser Terraform ou OpenTofu pour créer une VM dans Proxmox" :

Plusieurs méthodes existent pour créer une multitude de VM à la fois :
- dupliquer les fichiers .tf ;
- utiliser différents fichiers .tfvars ;
- utiliser un fichier .tfvars comportant plusieurs blocs de ressources ;
- créer et utiliser un module.
Je me focalise sur les trois premières méthodes, ne maîtrisant pas encore la dernière.
De plus, cela fait un moment que je ne crée plus les modèles à la main. Désormais, j'utilise les images "cloud" proposées par les éditeurs comme Canonical, Debian ou encore Fedora… Voici la liste des images que j'utilise :
- Arch Linux : https://geo.mirror.pkgbuild.com/images/
- Debian 12 : https://cdimage.debian.org/images/cloud/bookworm/
- Debian 13 : https://cdimage.debian.org/images/cloud/trixie/
- Fedora 41 : https://fedoraproject.org/cloud/download
- Ubuntu 22.04* : https://cloud-images.ubuntu.com/jammy/
- Ubuntu 24.04* : https://cloud-images.ubuntu.com/noble/
*
: pour les images Ubuntu, je n'utilise plus les versions minimal
, qui m'ont parfois posé un problème de stabilité et de dépendance avec podman ou docker.
Duplication des fichiers .tf
La duplication de fichiers .tf est une méthode simple pour créer plusieurs machines virtuelles, mais elle pose un problème en termes de maintenabilité et de rigueur. Cette approche consiste à copier un fichier vm.tf
, par exemple, qui contient la configuration des ressources souhaitées, puis à le renommer et à ajuster les valeurs nécessaires pour chaque nouvelle instance. Chaque duplicata doit comporter une modification du nom de la ressource (par exemple, "proxmox_virtual_environment_vm" "vm2"
) et des ajustements sur les arguments spécifiques à chaque machine virtuelle.
Bien que cette méthode puisse sembler efficace au départ, elle peut rapidement devenir lourde à gérer. Chaque fichier .tf
dupliqué doit être mis à jour individuellement en cas de changement dans la configuration globale ou lors d'ajustements nécessaires.
Cette méthode peut être envisageable pour de petites infrastructures hétérogènes, où la rapidité de déploiement prime la maintenabilité. Cependant, dans un contexte d'infrastructure plus complexe ou homogène, elle devient rapidement ingérable.
Utiliser différents fichiers .tfvars
Dans mon lab, j'ai souvent employé cette méthode qui combine la duplication de fichiers Terraform avec l'utilisation des fichiers .tfvars
. L'approche est assez simple : pour chaque machine virtuelle, on utilise un fichier distinct .tfvars
, tout en partageant une base commune définie dans un fichier .tf
central. Ce fichier principal contient les ressources et configurations standardisées utilisables par toutes les instances.
Chaque fichier .tfvars
spécifie alors les paramètres uniques pour chaque machine virtuelle, permettant ainsi d'individualiser certaines configurations tout en minimisant la duplication du code source dans le fichier .tf
. Cela offre une flexibilité accrue et simplifie certains aspects de la gestion des configurations variées.
Bien que cette méthode nécessite encore un effort de maintenabilité, elle réduit sensiblement le volume de code dupliqué directement dans les fichiers .tf
, en comparaison avec une simple duplication des fichiers entiers. Cette approche est particulièrement utile pour gérer un nombre modéré d'instances où chaque VM a besoin de quelques spécificités propres.
Si vous reprenez les fichiers saisis dans la page "Utiliser Terraform ou OpenTofu pour créer une VM dans Proxmox", voici ce que vous devriez avoir (pour l'exemple, il y aura deux machines virtuelles) :
Fichier "variables.tf" :
### variables.tf
# The DNS domain used for cloud-init configuration.
variable "cloudinit_dns_domain" {
type = string
default = "home.arpa"
description = "The DNS domain to be configured by cloud-init."
}
# A list of DNS servers used for cloud-init configuration.
variable "cloudinit_dns_servers" {
type = list(string)
description = "A list of DNS server addresses for cloud-init configuration. Provide the IP addresses as a list of strings, e.g., [\"9.9.9.9\", \"149.112.112.112\"] (without backslashes)."
}
# SSH keys to be added to the authorized_keys file during cloud-init setup.
variable "cloudinit_ssh_keys" {
type = list(string)
description = "SSH public keys to be included in the user's authorized_keys file."
}
# The username and password for the initial user account set up by cloud-init.
variable "cloudinit_user_account" {
type = string
description = "Username for the initial user account created by cloud-init."
}
# ID of the datastore where the VM will be stored.
variable "vm_datastore_id" {
type = string
description = "ID of the datastore to store the virtual machine disks."
}
# Format for the VM disk file (e.g., qcow2, vmdk).
variable "vm_disk_file_format" {
type = string
description = "File format of the virtual machine's disk image."
}
# Name or identifier for the node.
variable "node_name" {
type = string
description = "Name or identifier of the Proxmox node."
}
# API token for authenticating with the Proxmox Virtual Environment (PVE) host.
variable "pve_api_token" {
type = string
description = "API token used to authenticate with the PVE server. Used too into a PVE cluster"
}
# IP address or hostname of the PVE host.
variable "pve_host_address" {
type = string
description = "IP address or hostname of a PVE host."
}
variable "tags" {
type = list(string)
description = "A list of tags to label the VM."
}
# Directory path for temporary files during VM setup.
variable "tmp_dir" {
type = string
description = "Path to a directory used for storing temporary files like ISO uploads."
}
# Network bridge interface for connecting the VM's LAN.
variable "vm_bridge_lan" {
type = string
description = "Network bridge name for the virtual machine's LAN connection."
}
# Number of CPU cores assigned to the VM.
variable "vm_cpu_cores_number" {
type = number
description = "Number of CPU cores allocated to the virtual machine."
}
# Type of CPUs used in the VM (e.g., host, kvm).
variable "vm_cpu_type" {
type = string
description = "Type of CPU model assigned to the virtual machine."
}
# Description for the VM.
variable "vm_description" {
type = string
description = "Human-readable description or notes about the virtual machine. Markdown compatible."
}
# Size of the VM's disk in GB.
variable "vm_disk_size" {
type = number
description = "Size of the disk for the virtual machine, specified in gigabytes. E.g. If you want a 2 Gb disk, type '1863'."
}
variable "vm_gateway_ipv4" {
description = "The gateway IP address for the VM's network interface"
type = string
}
# Unique identifier for the VM instance (e.g., ID).
variable "vm_id" {
type = number
description = "Unique numerical identifier for the virtual machine. Limited to 9 digits"
}
variable "vm_ipv4_address" {
description = "The IPv4 address assigned to the VM, without its network mask."
type = string
}
# Maximum amount of memory allocated to the VM in MB.
variable "vm_memory_max" {
type = number
description = "Maximum RAM allocation for the virtual machine, specified in megabytes. E.g. for 4 Gb, type '4096'."
}
# Minimum amount of memory allocated to the VM in MB.
variable "vm_memory_min" {
type = number
description = "Minimum RAM allocation for the virtual machine, specified in megabytes. E.g. for 4 Gb, type '4096'."
}
# Name or label for identifying the VM.
variable "vm_name" {
type = string
description = "Name used to identify the virtual machine."
}
# Order to start the VM
variable "vm_startup_order" {
type = string
description = "A number to set the order of the VM starting."
}
# Number of CPU sockets allocated to the VM.
variable "vm_socket_number" {
type = number
description = "Number of CPU sockets assigned to the virtual machine. Don't assign more virtual sockets than you have physically."
}
Fichier "vm.tf" :
### vm.tf
terraform {
required_providers {
proxmox = {
source = "bpg/proxmox"
version = "~> 0.70"
}
random = {
source = "hashicorp/random"
version = "3.6.3"
}
}
}
provider "proxmox" {
endpoint = var.pve_host_address
api_token = var.pve_api_token
insecure = true
ssh {
agent = true
username = var.pve_api_user
}
tmp_dir = var.tmp_dir
}
# cloud-image Ubuntu 24 avec cloud-init
resource "proxmox_virtual_environment_download_file" "ubuntu24_cloudimg_20250117" {
checksum = "63f5e103195545a429aec2bf38330e28ab9c6d487e66b7c4b0060aa327983628"
checksum_algorithm = "sha256"
content_type = "iso"
datastore_id = var.datastore_id
file_name = "ubuntu-24.04-server-20250117-cloudimg-amd64.img"
node_name = var.node_name
upload_timeout = 360
url = "https://cloud-images.ubuntu.com/noble/20250117/noble-server-cloudimg-amd64.img"
}
resource "proxmox_virtual_environment_vm" "vm1_example" {
depends_on = [proxmox_virtual_environment_download_file.ubuntu24_cloudimg_20250117]
description = var.vm_description
keyboard_layout = "fr"
machine = "q35"
migrate = true
name = var.vm_name
node_name = var.node_name
scsi_hardware = "virtio-scsi-single"
started = true
stop_on_destroy = true # Force stop when destroying
tablet_device = false # VM without GUI
tags = var.tags
timeout_create = 180
timeout_shutdown_vm = 30
timeout_stop_vm = 30
vm_id = var.vm_id
agent {
enabled = true
timeout = "5m"
trim = true
}
cpu {
cores = var.vm_cpu_cores_number
flags = []
numa = true
sockets = var.vm_socket_number
type = var.vm_cpu_type
}
disk {
aio = "native"
cache = "none"
datastore_id = var.datastore_id
discard = "on"
file_format = var.disk_file_format
file_id = "${proxmox_virtual_environment_download_file.ubuntu24_cloudimg_20250117.datastore_id}:iso/${proxmox_virtual_environment_download_file.ubuntu24_cloudimg_20250117.file_name}"
interface = "scsi0"
iothread = true
replicate = false
size = var.vm_disk_size
}
efi_disk {
datastore_id = var.datastore_id
pre_enrolled_keys = false
type = "4m"
}
initialization {
datastore_id = var.datastore_id
dns {
domain = var.cloudinit_dns_domain
servers = var.cloudinit_dns_servers
}
ip_config {
ipv4 {
address = "${var.vm_ipv4_address}/16"
gateway = var.vm_gateway_ipv4
}
}
user_account {
keys = var.cloudinit_ssh_keys
username = var.cloudinit_user_account
}
}
memory {
dedicated = var.vm_memory_max
floating = var.vm_memory_min
}
network_device {
bridge = var.vm_bridge_lan
model = "virtio"
}
operating_system {
type = "l26"
}
startup {
order = var.vm_startup_order
up_delay = 15
down_delay = 30
}
serial_device {}
tpm_state {
datastore_id = var.datastore_id
version = "v2.0"
}
vga {
type = "virtio"
}
}
Fichier "vm1.tfvars" :
### vm1.tfvars
cloudinit_dns_domain = "your.domain.net"
cloudinit_dns_servers = ["9.9.9.9"]
cloudinit_ssh_keys = ["ssh-ed25519 changeme"]
cloudinit_user_account = "jho"
node_name = "pve"
pve_api_token = "terrabot@pve!token_name=token_secret"
pve_host_address = "https://pve:8006"
tags = ["linux", "opentofu"]
tmp_dir = "/tmp"
vm_bridge_lan = "vmbr0"
vm_cpu_cores_number = 2
vm_cpu_type = "x86-64-v2-AES"
vm_datastore_id = "local"
vm_description = "VM1 Managed by terraform."
vm_disk_file_format = "raw"
vm_disk_size = 64
vm_gateway_ipv4 = "172.16.0.254"
vm_id = 9993
vm_ipv4_address = "172.16.241.11"
vm_memory_max = 8192
vm_memory_min = 4096
vm_name = "vm1"
vm_startup_order = "1"
vm_socket_number = 1
Fichier "vm2.tfvars" :
### vm2.tfvars
cloudinit_dns_domain = "your.domain.net"
cloudinit_dns_servers = ["9.9.9.9"]
cloudinit_ssh_keys = ["ssh-ed25519 changeme"]
cloudinit_user_account = "jho"
node_name = "pve"
pve_api_token = "terrabot@pve!token_name=token_secret"
pve_host_address = "https://pve:8006"
tags = ["linux", "opentofu"]
tmp_dir = "/tmp"
vm_bridge_lan = "vmbr0"
vm_cpu_cores_number = 2
vm_cpu_type = "x86-64-v2-AES"
vm_datastore_id = "local"
vm_description = "VM2 Managed by terraform."
vm_disk_file_format = "raw"
vm_disk_size = 64
vm_gateway_ipv4 = "172.16.0.254"
vm_id = 9993
vm_ipv4_address = "172.16.241.12"
vm_memory_max = 8192
vm_memory_min = 4096
vm_name = "vm2"
vm_startup_order = "2"
vm_socket_number = 1
Une fois les fichiers créés, vous devrez ajouter un argument dans la ligne de commande tofu
pour planifier, appliquer ou détruire les changements.
# VM 1
tofu plan -var-file=vm1.tfvars -out vm1plan
tofu apply vm1plan
tofu destroy -var-file=vm1.tfvars
# VM 2
tofu plan -var-file=vm2.tfvars -out vm2plan
tofu apply vm2plan
tofu destroy -var-file=vm2.tfvars
Utiliser un fichier .tfvars comportant plusieurs blocs de ressources
À l'heure actuelle, c'est sans doute la méthode que je préfère. Elle repose sur l'utilisation de la boucle for_each
au sein des blocs resource
, offrant une approche efficace et épurée pour la gestion de multiples machines virtuelles. L'avantage principal réside dans la mutualisation des configurations, ce qui facilite grandement la maintenance. Grâce à un seul bloc de création de VM, il est possible de gérer théoriquement un nombre illimité d'instances en fonction de vos besoins.
Cette méthode requiert également l'utilisation d'un fichier contenant les variables générales et un autre fichier définissant les ressources essentielles. De plus, le fichier variables.auto.tfvars
joue un rôle crucial puisqu'il contient les blocs spécifiques pour chaque machine virtuelle souhaitée.
Fichier "variables.tf" - il y a une grosse variable qui comporte un sous-ensemble de différents arguments, chacun ayant ses valeurs.
variable "vm" {
type = map(object({
bridge = string
cpu_cores = number
cpu_type = string
description = string
disk_efi_datastore = string
disk_size = number
disk_vm_datastore = string
dns_servers = list(string)
domain = string
firewall_enabled = bool
gw = string
hostname = string
ipv4 = string
net_mac_address = string
net_rate_limit = number
node = string
ram_max = number
ram_min = number
start_on_boot = bool
started = bool
startup_order = string
tags = list(string)
vm_id = number
}))
}
Fichier "vm.tf" :
resource "proxmox_virtual_environment_download_file" "ubuntu24_cloudimg_20250117" {
checksum = "63f5e103195545a429aec2bf38330e28ab9c6d487e66b7c4b0060aa327983628"
checksum_algorithm = "sha256"
content_type = "iso"
datastore_id = each.value.disk_vm_datastore
file_name = "ubuntu-24.04-server-20250117-cloudimg-amd64.img"
node_name = each.value.node
upload_timeout = 180
url = "https://cloud-images.ubuntu.com/noble/20250117/noble-server-cloudimg-amd64.img"
}
resource "proxmox_virtual_environment_vm" "vm" {
depends_on = [proxmox_virtual_environment_download_file.ubuntu24_cloudimg_20250117]
for_each = var.vm
bios = "seabios"
description = each.value.description
keyboard_layout = "fr"
machine = "q35"
migrate = true
name = each.value.hostname
node_name = each.value.node
on_boot = each.value.start_on_boot
scsi_hardware = "virtio-scsi-single"
started = each.value.started
stop_on_destroy = true
tablet_device = false
tags = each.value.tags
timeout_create = 180
timeout_shutdown_vm = 30
timeout_stop_vm = 30
vm_id = each.value.vm_id
agent {
enabled = true
timeout = "5m"
trim = true
}
cpu {
cores = each.value.cpu_cores
numa = true
sockets = 1
type = each.value.cpu_type
}
disk {
aio = "native"
cache = "none"
datastore_id = each.value.disk_vm_datastore
discard = "on"
file_id = proxmox_virtual_environment_download_file.ubuntu24_cloudimg_20250117.id
iothread = true
interface = "scsi0"
replicate = false
size = each.value.disk_size
}
efi_disk {
datastore_id = each.value.disk_efi_datastore
pre_enrolled_keys = false
type = "4m"
}
initialization {
datastore_id = each.value.disk_vm_datastore
dns {
domain = each.value.domain
servers = each.value.dns_servers
}
ip_config {
ipv4 {
address = each.value.ipv4
gateway = each.value.gw
}
}
}
memory {
dedicated = each.value.ram_max
floating = each.value.ram_min
}
network_device {
bridge = each.value.bridge
firewall = each.value.firewall_enabled
mac_address = each.value.net_mac_address
rate_limit = each.value.net_rate_limit
}
operating_system {
type = "l26"
}
serial_device {}
startup {
order = each.value.startup_order
up_delay = 15
down_delay = 60
}
tpm_state {
datastore_id = each.value.datastore_id
version = "v2.0"
}
vga {
type = "virtio"
}
}
Et enfin, le fichier "parameters.auto.tfvars" (il est possible de le nommer autrement, cependant il doit garder l'extension .auto.tfvars
) :
vm = {
"vm01" = {
bridge = "vmbr0"
cpu_cores = 1
cpu_type = ""
description = "Managed by OpenTofu. VM1"
disk_efi_datastore = "zfsvm"
disk_size = 32
disk_vm_datastore = "zfsvm"
dns_servers = ["9.9.9.9", "1.1.1.1", "1.0.0.1"]
domain = "test.home.arpa"
firewall_enabled = false
gw = "192.168.1.254"
hostname = "vm01"
ipv4 = "192.168.1.21/24"
net_mac_address = "BC:24:11:D0:D0:21"
net_rate_limit = 0
node = "pve"
ram_max = 4096
ram_min = 4096
start_on_boot = true
started = true
startup_order = "1"
tags = ["opentofu", "ubuntu24"]
vm_id = 555501
}
"vm02" = {
bridge = "vmbr0"
cpu_cores = 1
cpu_type = ""
description = "Managed by OpenTofu. VM2"
disk_efi_datastore = "zfsvm"
disk_size = 32
disk_vm_datastore = "zfsvm"
dns_servers = ["9.9.9.9", "1.1.1.1", "1.0.0.1"]
domain = "test.home.arpa"
firewall_enabled = false
gw = "192.168.1.254"
hostname = "vm02"
ipv4 = "192.168.1.22/24"
net_mac_address = "BC:24:11:D0:D0:22"
net_rate_limit = 0
node = "pve"
ram_max = 4096
ram_min = 4096
start_on_boot = true
started = true
startup_order = "1"
tags = ["opentofu", "ubuntu24"]
vm_id = 555502
}
}
Au travers des commandes habituelles tofu plan -out vmplan
et un tofu apply vmplan
, vous pourrez déployer toutes les VM que composent les blocs dans le fichier "parameters.auto.tfvars". Vous pouvez cibler une VM particulière, en saisissant (par exemple) tofu plan -target=proxmox_virtual_environment_vm.vm["vm01"]
et son pendant tofu apply -target=proxmox_virtual_environment_vm.vm["vm01"]
.
Créer et utiliser un module
C'est aujourd'hui une des méthodes les plus plébiscités, grâce à sa modularité et sa portabilité. Les modules peuvent être partagés et réutilisés aisément. Aujourd'hui, je n'ai pas encore pris le temps d'essayer et de mettre en œuvre les modules - c'est dans la to-do list…