Quand j’embarque un nouveau client, je lui monte un « VPC » dédié dans mon infra Proxmox : un petit ensemble de conteneurs LXC isolés qui hébergent sa stack. Pour un projet PHP typique, ça donne 3 nœuds PHP, un MariaDB master/slave et un HAProxy devant. Faire ça à la main dans l’interface Proxmox, conteneur par conteneur, nœud par nœud, c’est long et surtout pas reproductible. Voici comment Terraform (couplé à Ansible) a réglé le problème — et le piège qui m’a rattrapé une fois en production.
Le besoin : un VPC reproductible, créable et destructible à volonté
En phase de montage, on tâtonne : on ajuste la RAM d’un nœud PHP, on teste le placement du slave MariaDB sur un autre hôte, on recommence. Cliquer dans l’UI Proxmox à chaque itération, c’est l’erreur humaine garantie et zéro trace de ce qu’on a fait. Je voulais décrire le VPC une fois, dans du code versionné, et pouvoir le créer ou détruire d’une commande.
C’est exactement le terrain de l’infrastructure as code. Terraform décrit l’état souhaité, calcule le différentiel et provisionne tout d’un coup. Proxmox expose une API REST ; reste à les relier.
Le provider Proxmox (et un avertissement)
J’utilise le provider historique Telmate/proxmox. Honnêteté d’abord : en 2026, ce n’est plus le choix que je recommanderais pour un nouveau projet. Il est quasi figé, et la communauté a basculé sur bpg/proxmox, bien plus complet et activement maintenu. Mes VPC existants tournent sous Telmate, donc c’est ce que je montre ici — mais si vous démarrez, regardez bpg en premier (j’y reviens en fin d’article).
Premier réflexe de sécurité : on s’authentifie par token d’API, pas avec le mot de passe root@pam. Le provider, dans un providers.tf :
terraform {
required_providers {
proxmox = {
source = "Telmate/proxmox"
version = "~> 3.0"
}
}
}
provider "proxmox" {
pm_api_url = "https://pve.mon-infra.fr:8006/api2/json"
pm_api_token_id = var.pm_api_token_id
pm_api_token_secret = var.pm_api_token_secret
pm_tls_insecure = false
}
Décrire tout le VPC d’un coup
Plutôt qu’un count anonyme, je décris chaque rôle dans une map et je boucle avec for_each : les conteneurs ont des noms parlants dans l’état Terraform (php-1, db-master…) et non un index opaque. La répartition sur les nœuds est explicite.
locals {
vpc = "client-acme"
lxc = {
php-1 = { node = "pve-1", cores = 4, memory = 4096, ip = "10.20.0.11/24" }
php-2 = { node = "pve-2", cores = 4, memory = 4096, ip = "10.20.0.12/24" }
php-3 = { node = "pve-3", cores = 4, memory = 4096, ip = "10.20.0.13/24" }
db-master = { node = "pve-1", cores = 4, memory = 8192, ip = "10.20.0.21/24" }
db-slave = { node = "pve-2", cores = 4, memory = 8192, ip = "10.20.0.22/24" }
haproxy = { node = "pve-3", cores = 2, memory = 2048, ip = "10.20.0.10/24" }
}
}
resource "proxmox_lxc" "vpc" {
for_each = local.lxc
target_node = each.value.node
hostname = "${local.vpc}-${each.key}"
ostemplate = "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst"
unprivileged = true
cores = each.value.cores
memory = each.value.memory
rootfs {
storage = "local-lvm"
size = "16G"
}
network {
name = "eth0"
bridge = "vmbr0"
ip = each.value.ip
gw = "10.20.0.1"
}
ssh_public_keys = file("~/.ssh/id_ed25519.pub")
}
Six conteneurs répartis sur trois nœuds, décrits en une trentaine de lignes. Le cycle habituel suffit ensuite :
terraform init
terraform plan
terraform apply
Le VPC complet sort en quelques minutes, là où le montage manuel dans l’UI me prenait une bonne demi-journée — et avec la certitude que les six conteneurs sont strictement identiques d’un client à l’autre. Surtout, un terraform destroy nettoie tout : pendant la phase de montage, je crée et je détruis le VPC entier sans état d’âme.
Et Ansible prend le relais
Terraform s’arrête au conteneur vide. La configuration — version de PHP, réplication MariaDB master/slave, backends HAProxy — c’est le job d’Ansible. Terraform expose les IP en sortie :
output "vpc_ips" {
value = { for k, c in proxmox_lxc.vpc : k => c.network[0].ip }
}
De là, je génère l’inventaire Ansible et les playbooks appliquent une config identique entre les nœuds. C’est le duo qui fait tout l’intérêt : Terraform garantit que l’infra est reproductible, Ansible que la configuration l’est aussi. Deux VPC clients montés à six mois d’écart sont rigoureusement jumeaux.
Le piège : migrer un LXC entre nœuds en prod
Tout va bien… jusqu’au jour où, en production, on doit migrer un conteneur d’un nœud Proxmox à un autre — rééquilibrage de charge, maintenance d’un hôte, panne. On fait la migration depuis Proxmox, proprement. Et là, au terraform plan suivant, c’est la douche froide :
# php-2 a été migré de pve-2 vers pve-3 dans Proxmox
$ terraform plan
# proxmox_lxc.vpc["php-2"] must be replaced
-/+ resource "proxmox_lxc" "vpc" {
~ target_node = "pve-2" -> "pve-3" # forces replacement
}
Plan: 1 to add, 0 to change, 1 to destroy.
Terraform veut détruire et recréer le conteneur, parce que pour lui le target_node de son état (pve-2) ne correspond plus à la réalité (pve-3), et que ce champ force le remplacement. En prod, sur une base MariaDB ou un nœud PHP en service, c’est exactement ce qu’on ne veut pas.
La racine du problème est conceptuelle : Terraform raisonne en état désiré déclaratif, alors qu’une migration live est une action impérative décidée hors de son périmètre. Les deux visions du monde se télescopent.
La parade que j’applique : dire à Terraform d’ignorer le placement une fois le VPC monté.
resource "proxmox_lxc" "vpc" {
for_each = local.lxc
# ...
lifecycle {
ignore_changes = [target_node]
}
}
Terraform ne surveille plus sur quel nœud tourne le conteneur : on peut migrer librement dans Proxmox sans qu’un plan ne menace de tout recréer. La contrepartie, assumée : le placement n’est plus géré par l’IaC, il devient une affaire d’exploitation (day-2 ops) côté Proxmox. Pour mon usage, c’est le bon compromis.
Ce que j’en retiens
- Terraform brille au montage et au teardown. Pour bâtir un VPC client reproductible et le détruire à volonté pendant qu’on itère sur l’archi, c’est imbattable.
- Pour le placement en prod, ce n’est pas son terrain. Dès qu’on migre des LXC entre nœuds à la main, l’état Terraform diverge et veut tout recréer.
ignore_changes = [target_node]est la rustine — au prix de sortir le placement du périmètre IaC. - Terraform + Ansible, c’est le duo gagnant. L’un provisionne l’infra, l’autre garantit une configuration identique entre nœuds. L’un sans l’autre laisse la moitié du travail à la main.
- Nouveau projet ? Partez sur
bpg/proxmox. Telmate fait encore le travail sur mes VPC en place, mais bpg est désormais le standard maintenu — et il gère bien plus de ressources Proxmox.