Général 10 min de lecture · 1 988 mots

Terraform + Proxmox Provider : Infrastructure as Code pour ton homelab

Terraform + Proxmox Provider : Infrastructure as Code pour ton homelab
Général · 2026.08.12

Après deux ans à créer des VMs et LXC à la main dans l’interface Proxmox, un schéma s’est installé : tu te souviens que tu as créé un container pour tester quelque chose, tu ne te souviens plus de ses paramètres exacts, et quand tu veux le recréer ailleurs ou l’auditer, c’est une chasse aux informations dans l’interface. Tu lis la configuration dans les panneaux Proxmox, tu notes quelque chose, tu espères ne rien oublier.

L’Infrastructure as Code appliquée au homelab, c’est simplement inverser le flux : la définition de ce qui doit exister vit dans des fichiers versionnés dans Git, et l’outil (Terraform) se charge de faire coïncider l’état réel avec cette définition.

J’avais déjà Ansible pour le provisioning des VMs – installer des packages, déployer des configs, gérer les services. Terraform vient avant : il déclare quelles machines doivent exister, avec quels paramètres réseau et ressources. Les deux sont complémentaires et ne font pas le même travail.

Choix du provider Proxmox

Deux providers Terraform pour Proxmox coexistent dans l’écosystème :

  • Telmate/proxmox : le plus ancien, longtemps le seul disponible. Maintenance ralentie depuis 2023, support Proxmox 8.x partiel.
  • bpg/proxmox : maintenu activement, supporte Proxmox 8.x complètement, API plus complète, mises à jour régulières.

J’utilise bpg/proxmox. La version 0.66.x au moment de cet article.

Créer le token API Proxmox

Terraform a besoin d’un token API Proxmox pour interagir avec le cluster. Ne pas utiliser root – créer un utilisateur dédié avec les droits minimaux nécessaires.

# Sur le nœud Proxmox, en root
pveum user add terraform@pve --comment "Terraform IaC user"
pveum role add TerraformRole --privs 
  "VM.Allocate VM.Clone VM.Config.CDROM VM.Config.CPU VM.Config.Cloudinit 
  VM.Config.Disk VM.Config.HWType VM.Config.Memory VM.Config.Network 
  VM.Config.Options VM.Monitor VM.Audit VM.PowerMgmt 
  Datastore.AllocateSpace Datastore.Audit 
  SDN.Use Pool.Audit Sys.Audit"
pveum acl modify / --user terraform@pve --role TerraformRole --propagate 1

# Créer le token API
pveum user token add terraform@pve homelab --privsep 0

La dernière commande affiche le token secret une seule fois. Le noter immédiatement dans un gestionnaire de secrets (Vaultwarden dans mon cas).

Le --privsep 0 désactive la séparation de privilèges du token, ce qui lui donne les mêmes droits que l’utilisateur. Avec --privsep 1 (défaut), le token ne peut accéder qu’aux endpoints autorisés explicitement – ce qui bloque certaines opérations du provider bpg/proxmox sans message d’erreur clair. Trouver ce comportement m’a coûté une après-midi.

Structure du projet

terraform/
├── providers.tf
├── variables.tf
├── lxc-pihole.tf
├── vm-ubuntu.tf
└── terraform.tfvars   # non versionné (.gitignore)

Le state est stocké localement (terraform.tfstate). Pour un homelab à un seul opérateur, un backend remote (S3, Consul) est overkill et ajoute une dépendance. Le fichier state vit dans le répertoire du projet, versionné dans Git uniquement quand il n’y a pas de secrets dedans – en pratique, j’ajoute terraform.tfstate* au .gitignore et je fais des backups manuels vers le NAS.

providers.tf

# terraform/providers.tf
terraform {
  required_version = ">= 1.9.0"

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

provider "proxmox" {
  endpoint = var.proxmox_endpoint
  api_token = var.proxmox_api_token
  insecure  = true  # certificat auto-signé Proxmox

  ssh {
    agent    = true
    username = "root"
  }
}

Le bloc ssh est nécessaire pour certaines opérations du provider bpg/proxmox qui ne passent pas par l’API REST mais directement par SSH – notamment la copie de fichiers lors du provisioning Cloud-init.

variables.tf

# terraform/variables.tf
variable "proxmox_endpoint" {
  type        = string
  description = "URL de l'API Proxmox"
  default     = "https://10.10.1.1:8006"
}

variable "proxmox_api_token" {
  type        = string
  sensitive   = true
  description = "Token API Proxmox au format 'user@realm!tokenid=secret'"
}

variable "proxmox_node" {
  type        = string
  description = "Nom du nœud Proxmox cible"
  default     = "heighliner"
}

terraform.tfvars (non versionné)

# terraform/terraform.tfvars  - dans .gitignore
proxmox_api_token = "terraform@pve!homelab=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

Exemple 1 : LXC Pi-hole déclaratif

# terraform/lxc-pihole.tf
resource "proxmox_virtual_environment_lxc" "pihole" {
  node_name = var.proxmox_node
  vm_id     = 150
  tags      = ["dns", "infrastructure", "terraform"]

  description = "Pi-hole DNS sinkhole - géré par Terraform"

  initialization {
    hostname = "pihole"

    ip_config {
      ipv4 {
        address = "10.10.1.150/24"
        gateway = "10.10.1.1"
      }
    }

    user_account {
      password = var.pihole_root_password
      keys     = [var.ssh_public_key]
    }
  }

  cpu {
    cores = 1
  }

  memory {
    dedicated = 512
  }

  disk {
    datastore_id = "local-lvm"
    size         = 8
  }

  network_interface {
    name   = "eth0"
    bridge = "vmbr0"
    vlan_id = 10  # VLAN LAN
  }

  operating_system {
    template_file_id = "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst"
    type             = "debian"
  }

  start_on_boot = true
  started       = true
  unprivileged  = true
}

Ajouter les variables correspondantes dans variables.tf :

variable "pihole_root_password" {
  type      = string
  sensitive = true
}

variable "ssh_public_key" {
  type = string
}

Exemple 2 : VM Ubuntu via template Cloud-init

Pour les VMs, le workflow est différent : Terraform clone un template Cloud-init créé à la main une seule fois sur Proxmox. Le template est créé manuellement, puis Terraform s’en sert comme base pour créer des VMs configurables.

Créer le template manuellement (une fois) :

# Sur le nœud Proxmox
wget -q https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img 
  -O /tmp/noble-cloudimg.img

qm create 9000 
  --name ubuntu-24.04-cloud 
  --memory 1024 
  --cores 1 
  --net0 virtio,bridge=vmbr0

qm importdisk 9000 /tmp/noble-cloudimg.img local-lvm
qm set 9000 
  --scsihw virtio-scsi-pci 
  --scsi0 local-lvm:vm-9000-disk-0 
  --ide2 local-lvm:cloudinit 
  --boot c 
  --bootdisk scsi0 
  --serial0 socket 
  --agent enabled=1

qm template 9000

Ce template (VM ID 9000) ne démarre jamais directement – il sert uniquement de base pour les clones.

Déclarer la VM dans Terraform :

# terraform/vm-ubuntu.tf
resource "proxmox_virtual_environment_vm" "ubuntu_test" {
  node_name = var.proxmox_node
  vm_id     = 201
  name      = "ubuntu-test"
  tags      = ["ubuntu", "test", "terraform"]

  description = "VM de test Ubuntu 24.04 - géré par Terraform"

  clone {
    vm_id = 9000  # template Cloud-init
    full  = true
  }

  cpu {
    cores   = 2
    sockets = 1
    type    = "x86-64-v2-AES"
  }

  memory {
    dedicated = 2048
  }

  disk {
    datastore_id = "local-lvm"
    interface    = "scsi0"
    size         = 20
    discard      = "on"
    ssd          = true
  }

  network_device {
    bridge  = "vmbr0"
    vlan_id = 30  # VLAN Serveurs
    model   = "virtio"
  }

  initialization {
    ip_config {
      ipv4 {
        address = "10.10.30.201/24"
        gateway = "10.10.30.1"
      }
    }

    user_account {
      username = "ubuntu"
      password = var.vm_default_password
      keys     = [var.ssh_public_key]
    }

    dns {
      servers = ["10.10.1.150"]  # Pi-hole
      domain  = "arewel.com"
    }
  }

  agent {
    enabled = true
  }

  on_boot = true
}

Utiliser Terraform

Initialiser le projet :

cd terraform/
terraform init
Initializing the backend...
Initializing provider plugins...
- Finding bpg/proxmox versions matching "~> 0.66"...
- Installing bpg/proxmox v0.66.3...
- Installed bpg/proxmox v0.66.3 (signed, key ID ...)

Terraform has been successfully initialized!

Planifier les changements :

terraform plan

Exemple d’output pour la création du LXC Pi-hole :

Terraform will perform the following actions:

  # proxmox_virtual_environment_lxc.pihole will be created
  + resource "proxmox_virtual_environment_lxc" "pihole" {
      + id        = (known after apply)
      + node_name = "heighliner"
      + started   = true
      + tags      = ["dns", "infrastructure", "terraform"]
      + vm_id     = 150

      + cpu {
          + cores = 1
        }

      + disk {
          + datastore_id = "local-lvm"
          + size         = 8
        }

      + initialization {
          + hostname = "pihole"
          ...
        }

      + memory {
          + dedicated = 512
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

terraform plan ne fait rien – il prédit uniquement. L’appliquer :

terraform apply

Terraform demande confirmation avant d’agir. Taper yes. La création du LXC prend environ 90 secondes :

proxmox_virtual_environment_lxc.pihole: Creating...
proxmox_virtual_environment_lxc.pihole: Still creating... [10s elapsed]
proxmox_virtual_environment_lxc.pihole: Creation complete after 1m23s [id=heighliner/150]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Le LXC 150 est maintenant visible dans l’interface Proxmox, démarré, avec les paramètres définis dans lxc-pihole.tf.

Pour supprimer proprement une ressource :

terraform destroy -target proxmox_virtual_environment_lxc.pihole

terraform destroy sans -target détruira toutes les ressources gérées par Terraform dans ce projet. Toujours utiliser -target pour supprimer une ressource spécifique, sauf si tu veux vraiment tout détruire.

La frontière entre Terraform et Ansible

Une confusion fréquente quand on commence avec ces deux outils : à quoi sert chacun ?

Terraform répond à la question « qu’est-ce qui doit exister ? » : un LXC avec tels paramètres réseau, telle mémoire, tel template de base. Il est idempotent sur l’état déclaré – si le LXC existe déjà avec les bons paramètres, terraform apply ne fait rien.

Ansible répond à « comment configurer ce qui existe ? » : installer Pi-hole dans ce LXC, configurer ses listes de blocage, créer le fichier /etc/pihole/setupVars.conf avec les bons paramètres. Il agit sur une machine qui tourne déjà.

Le workflow naturel dans mon homelab est devenu :

  1. Écrire la définition Terraform de la nouvelle machine
  2. terraform apply – la machine existe, elle tourne, on peut s’y connecter en SSH
  3. Passer un rôle Ansible sur l’IP de la nouvelle machine – le service est installé et configuré

Terraform ne sait pas ce qui tourne à l’intérieur d’une VM. Ansible ne sait pas comment la VM a été créée. Chacun reste dans son couloir, et les deux fichiers (.tf et playbook Ansible) sont versionnés dans Git.

Ajouter une output Terraform qui imprime l’IP de chaque machine créée facilite l’intégration avec les playbooks Ansible. Tu peux même utiliser terraform output -json pour générer dynamiquement un inventaire Ansible.

Ce qui a coincé

La liste des permissions API nécessaires pour le provider bpg/proxmox n’est pas documentée de façon exhaustive dans le README du provider. La documentation principale liste les permissions génériques, mais certaines opérations spécifiques (Cloud-init, resize de disque, configuration VLAN) nécessitent des droits supplémentaires.

La première fois que j’ai essayé de créer une VM via un clone Cloud-init, Terraform s’est arrêté avec une erreur cryptique :

Error: unable to clone VM
...
500 Internal Server Error: ...  permission check failed (403 Permission check failed)

La permission manquante était VM.Config.Cloudinit. Je l’ai trouvée en cherchant dans les issues GitHub du provider – pas dans la documentation principale. La liste complète que j’utilise est celle donnée dans la section « Créer le token API » plus haut – elle inclut déjà VM.Config.Cloudinit après avoir trouvé ce problème.

Le deuxième écueil concerne le state Terraform et les modifications manuelles dans l’interface Proxmox. Si tu modifies un LXC dans l’UI Proxmox (changer la RAM par exemple) sans mettre à jour le fichier .tf correspondant, le prochain terraform apply va détecter la différence et proposer de remettre la ressource dans l’état déclaré. C’est le comportement attendu – c’est la définition même de l’IaC – mais ça peut surprendre si tu n’as pas l’habitude.

La règle que je me suis fixée : toute modification d’une ressource gérée par Terraform se fait dans le fichier .tf, jamais dans l’interface Proxmox. Pour tester rapidement une option dans l’UI (avant de la déclarer), terraform state rm permet de sortir temporairement une ressource du state sans la détruire.

Résultat

Créer un nouveau LXC avec la configuration complète (réseau, ressources, SSH key) prend maintenant 2 minutes : écrire 20-30 lignes de HCL, terraform apply, le container est là.

L’état de l’infrastructure est versionné dans Git. Un git log terraform/ donne l’historique de toutes les machines créées, modifiées ou supprimées, avec qui a fait le changement et pourquoi. Pour un homelab solo, c’est surtout utile pour retrouver « comment j’avais configuré cette VM il y a 8 mois » sans fouiller dans l’interface Proxmox.

La prochaine étape que j’envisage : utiliser un inventaire Ansible dynamique généré depuis le state Terraform, pour que les playbooks de provisioning s’appliquent automatiquement sur les nouvelles machines au moment de leur création.

Une remarque, un retour ?

Cet article est vivant - corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.

Laisser un commentaire