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" :

Utiliser Terraform ou OpenTofu pour créer une VM dans Proxmox
Suite à un article portant sur l’utilisation de Terraform pour créer des LXC pour Proxmox, je souhaite approfondir le sujet en abordant cette fois la création de machines virtuelles. Prérequis : Une connaissance préalable de Terraform, des droits d’administration sur Proxmox, et la capacité à effectuer des manipulations sans contraintes majeures.

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 :

* : 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 .tfdupliqué 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…

Share this post