skip to content

Recherche

Syspirit
FR

#Terraform sur vSphere : l'IaC n'est pas réservée au cloud

Déployer des VM avec Terraform sur un vCenter, oui ça se fait et ça marche bien. Retour sur ma découverte de l'Infra-as-Code en environnement VMware.

18 minutes de lecture Karl Certa
Terraform sur vSphere, l'IaC n'est pas réservée au cloud - Cover
Image générée par IA

L’Infra-as-Code était inconnu pour moi il y a deux ans. Quand on m’a fait découvrir le sujet lors d’une expérience pro, j’ai tout de suite accroché. Ma mission : automatiser le déploiement de nos serveurs virtuels sur les différents vCenter.

L’outil choisi, Terraform. Et là, premier doute. Tout ce que j’avais lu dessus parlait de cloud public : AWS, Azure, GCP. Or nos machines à nous vivaient sur du VMware on-premise, avec plusieurs vCenter et des VM Windows et Linux à sortir régulièrement.

Est-ce que l’Infrastructure as Code, c’est réservé à ceux qui ont du cloud public ?

Non. Et c’est ce que je vais vous raconter.

Le provider vSphere existe bel et bien


Terraform ne parle à aucune infrastructure directement. Il passe par un provider, une brique qui traduit ses instructions en appels d’API pour une plateforme donnée. Il en existe pour AWS, Azure, GCP, Docker, et donc pour vSphere.

Celui-ci sait faire l’essentiel de ce qu’on lui demande au quotidien : cloner un template, dimensionner la VM, la brancher sur le bon port group (le réseau virtuel, côté VMware), la poser sur le bon datastore (l’espace de stockage), la ranger dans le bon dossier d’inventaire, lui coller ses tags et la customiser au premier boot.

vsphere_prov.tf
terraform {
  required_providers {
    vsphere = {
      source  = "vmware/vsphere"
      version = "2.16.1"
    }
  }
}

La connexion au vCenter va dans un fichier à part :

base.tf
provider "vsphere" {
  user                 = var.vsphere_user
  password             = var.vsphere_password
  vsphere_server       = var.vsphere_server
  allow_unverified_ssl = true
}

Le allow_unverified_ssl mérite un mot. Les vCenter présentaient des certificats auto-signés, non intégrés au magasin de la machine de déploiement, donc pas vraiment le choix. Dans l’idéal, on y embarque la CA interne et on laisse ce flag à false.

Et surtout : les identifiants ne doivent jamais finir dans un fichier. Chez nous, la contrainte était double. Les comptes vCenter étaient nominatifs, parce qu’il fallait pouvoir tracer qui avait déployé quoi, et il était hors de question de les écrire en dur quelque part.

Terraform lit toute variable d’environnement préfixée TF_VAR_, ce qui règle une partie du problème : les identifiants restent dans le shell et ne touchent jamais le disque.

export TF_VAR_vsphere_user='[email protected]'
export TF_VAR_vsphere_password='...'

On verra plus loin comment le formulaire les récupère au lancement. Mais soyons clairs sur la vraie réponse : c’est un coffre. Un Vault aurait été plus que nécessaire ici, et c’est le premier manque de ce projet.

La structure d’un projet, et le déclic


C’est en montant mon premier projet que j’ai compris la logique de l’outil. Terraform, ce n’est pas un gros fichier, c’est une poignée de fichiers avec chacun son rôle.

projet/
├── vsphere_prov.tf     → quelle version du provider
├── base.tf             → à qui je parle (le vCenter)
├── main.tf             → ce que je veux (les VM)
├── variables.tf        → les paramètres possibles
└── terraform.tfvars    → les valeurs pour ce déploiement précis

La séparation entre variables.tf et terraform.tfvars est le truc qui m’a fait tilter. Le premier déclare ce qui est paramétrable, le second remplit ces paramètres pour un déploiement donné. Le main.tf, lui, ne bouge jamais : c’est la mécanique. Le tfvars, c’est la commande du jour. Une fois qu’on a saisi ça, on arrête d’écrire du Terraform à chaque déploiement et on se contente de le nourrir.

L’ordre de priorité des variables

Un détail qui fait perdre du temps à tout le monde au moins une fois : l’ordre de priorité des variables. Une même variable peut être définie à plusieurs endroits, et Terraform applique une hiérarchie stricte.

PrioritéSource
🥇 La plus forteLa ligne de commande : -var et -var-file
2Les fichiers *.auto.tfvars, dans l’ordre alphabétique
3Le fichier terraform.tfvars.json
4Le fichier terraform.tfvars
5Les variables d’environnement TF_VAR_...
🥉 La plus faibleLa valeur default du bloc variable

Le piège, c’est que l’intuition va souvent dans l’autre sens : on croit que ce qu’on exporte dans son shell prime, alors que c’est presque le dernier maillon. Quand une valeur refuse de changer alors que vous éditez le bon fichier, c’est qu’un cran plus haut l’écrase.

Ce que ça donne concrètement


Le main.tf commence par résoudre ce qui existe déjà dans le vCenter. Ce sont les blocs data : ils ne créent rien, ils traduisent un nom en identifiant interne.

main.tf
data "vsphere_datacenter" "datacenter" {
  name = var.vsphere_dc_name
}
 
data "vsphere_datastore" "datastore" {
  name          = var.vsphere_datastore
  datacenter_id = data.vsphere_datacenter.datacenter.id
}

Même principe pour le cluster (vsphere_compute_cluster), le réseau (vsphere_network) et le template (vsphere_virtual_machine).

Vient ensuite la ressource. Et là, un choix qui change tout : plutôt qu’un count qui sort N machines identiques, j’ai utilisé un for_each sur une map d’objets. Chaque VM devient une entrée nommée, avec ses propres specs.

terraform.tfvars
vm_config = {
  "SRVAPP01" = {
    cpu = 4, ram = 8192, ip = "10.0.10.21"
    system_domainname = "exemple.local"
    annotation        = "Back applicatif"
    disks_size        = [10, 5]
  },
  "SRVAPP02" = {
    cpu = 2, ram = 4096, ip = "10.0.10.22"
    system_domainname = "exemple.local"
    annotation        = "Front applicatif"
    disks_size        = [20]
  }
}

Un seul apply sort les deux machines, chacune avec sa taille, son IP et ses disques. C’est la différence entre « déploie-moi 3 VM » et « déploie-moi cette infra ».

La ressource pioche dans each.key (le nom) et each.value (les specs) :

resource "vsphere_virtual_machine" "vm" {
  for_each = var.vm_config
 
  name             = each.key
  resource_pool_id = data.vsphere_compute_cluster.cluster.resource_pool_id
  datastore_id     = data.vsphere_datastore.datastore.id
  guest_id         = data.vsphere_virtual_machine.template.guest_id
  annotation       = each.value.annotation
 
  num_cpus               = each.value.cpu
  memory                 = each.value.ram
  cpu_hot_add_enabled    = true
  memory_hot_add_enabled = true
 
  network_interface {
    network_id = data.vsphere_network.network.id
  }
 
  # Le disque système, repris du template
  disk {
    label            = "disk0"
    unit_number      = 0
    size             = data.vsphere_virtual_machine.template.disks[0].size
    thin_provisioned = data.vsphere_virtual_machine.template.disks[0].thin_provisioned
  }
 
  # Puis autant de disques que demandé
  dynamic "disk" {
    for_each = { for idx, size in each.value.disks_size : idx => size }
 
    content {
      label            = "disk${disk.key + 1}"
      unit_number      = disk.key + 1
      size             = disk.value
      thin_provisioned = true
    }
  }
}

Les deux hot_add_enabled sont un petit confort qui rapporte gros : ils autorisent l’ajout de CPU et de RAM à chaud, sans arrêter la machine le jour où elle est sous-dimensionnée. Et le bloc dynamic génère autant de disques que la liste disks_size en contient : [10, 5] sort deux disques de 10 et 5 Go, numérotés proprement.

Hostname, IP et domaine dans la VM

La customisation au premier boot passe par le bloc customize, à l’intérieur du clone. Il pousse le hostname, le domaine et la config réseau dans la VM au moment où elle démarre :

  clone {
    template_uuid = data.vsphere_virtual_machine.template.id
 
    customize {
      linux_options {
        host_name = each.key
        domain    = each.value.system_domainname
      }
 
      network_interface {
        ipv4_address = each.value.ip
        ipv4_netmask = var.netmask
      }
 
      ipv4_gateway    = var.gw
      dns_server_list = var.dns_server
    }
  }

Pour du Windows, on remplace linux_options par windows_options avec un computer_name.

Les tags vSphere

Dernier point que je n’attendais pas et qui s’est révélé très pratique : le provider sait poser les tags vSphere à la création, en résolvant d’abord la catégorie (vsphere_tag_category) puis le tag (vsphere_tag). Chez nous ils servaient notamment à la sauvegarde. Ce sont typiquement les tags qu’on oublie de poser à la main, et qu’on cherche six mois plus tard en se demandant pourquoi la machine n’est pas sauvegardée.

Le vrai problème n’est pas Terraform


Techniquement, à ce stade, tout marche. Sauf qu’il y a un hic, et c’est lui qui a occupé le plus clair de mon temps.

Pour déployer, Terraform veut le nom exact du datastore. Le nom exact du port group. Le chemin exact du dossier d’inventaire. Au caractère près. Et ces noms, personne ne les connaît par cœur : ils changent d’un vCenter à l’autre, d’un site à l’autre. Une faute de frappe, et le apply se casse la figure après vous avoir fait attendre.

Multipliez par plusieurs vCenter, plusieurs sites, plusieurs VLAN avec des configurations réseau différentes, et vous obtenez un outil que même moi je n’avais pas envie de lancer sans garder le client web ouvert à côté pour recopier les noms.

Un outil d’automatisation qu’on n’ose pas lancer n’automatise rien.

Demander au vCenter avant de demander à l’humain


L’idée du formulaire ne vient pas de moi. C’est un collègue avec qui je bossais sur le sujet qui me l’a soufflée : un script bash qui pose les questions et alimente le terraform.tfvars à notre place, en s’appuyant sur une base d’informations écrite en dur pour éviter les erreurs de saisie.

Ça fiabilise, mais ça ne tient pas dans le temps. Une liste de datastores ou de port groups codée dans un script devient fausse au premier changement côté infra, et personne ne pense à la mettre à jour. D’où le principe que j’y ai ajouté, et qui est devenu le cœur du truc : arrêter de demander à l’utilisateur ce que le vCenter sait déjà.

Les identifiants, saisis au lancement

Première chose que fait le formulaire, une fois le vCenter cible identifié : demander les identifiants, et les exporter pour Terraform.

echo -n "Entrez le nom d'utilisateur pour $chosen_vcenter_name : "
read -r VCENTER_USER
echo -n "Entrez le mot de passe pour $chosen_vcenter_name : "
read -rs VCENTER_PASSWORD
 
export TF_VAR_vsphere_user="$VCENTER_USER"
export TF_VAR_vsphere_password="$VCENTER_PASSWORD"

C’est la réponse à la contrainte de traçabilité évoquée plus haut : chacun déploie avec son propre compte, saisi au lancement. Le -s de read évite d’afficher le mot de passe à l’écran, et les deux export suffisent à ce que Terraform les retrouve le moment venu, sans qu’ils touchent jamais le disque.

Ces mêmes identifiants servent ensuite à interroger le vCenter. Et c’est là que mon script pèche, je ne l’ai réalisé qu’en écrivant cet article : il les passe en argument au script Python qui suit.

python3 script_import.py "$vcenter_host" "$VCENTER_USER" "$VCENTER_PASSWORD"

Les arguments d’un processus sont lisibles par les autres utilisateurs de la machine : un simple ps pendant l’exécution, et le mot de passe s’affiche. La fenêtre est courte et il faut déjà un accès à la machine de déploiement, mais le correctif est gratuit. Il suffit de passer par l’entrée standard, ou de laisser le script Python lire les variables TF_VAR_ qui viennent justement d’être exportées.

C’est typiquement le genre de détail qu’un Vault évite d’avoir à penser.

L’inventaire, lu en direct

Avant de proposer le moindre choix technique, ce script Python se connecte au vCenter et récupère l’inventaire réel, via pyVmomi pour l’essentiel et l’API REST pour les tags, qui ne sont pas exposés de la même façon.

script_import.py
from pyVmomi import vim
from pyVim.connect import SmartConnect, Disconnect
import requests
import ssl
import json
import sys
from requests.auth import HTTPBasicAuth
 
...
 
si = SmartConnect(host=vcenter_host, user=user, pwd=password, sslContext=context)
content = si.RetrieveContent()
 
def list_names(obj_type):
    view = content.viewManager.CreateContainerView(content.rootFolder, [obj_type], True)
    return [o.name for o in view.view]
 
info = {
    "vcenter_compute_clusters": list_names(vim.ClusterComputeResource),
    "vlan_info":                list_names(vim.Network),
    "datastore_info":           list_names(vim.Datastore),
}

Extrait du script pyVmomi

Le script sort un JSON avec les datacenters, les clusters, les port groups, les datastores, les templates disponibles, l’arborescence des dossiers et les tags rangés par catégorie.

Choisir dans une liste

Un script bash fait ensuite office de formulaire. Il lit ce JSON avec jq et ne propose que ce qui existe réellement :

echo "$vcenter_data" | jq -r '.vlan_info[]' | sort | nl -v 1
read -p "Entrez le numéro du VLAN choisi : " vlan_choice
selected_vlan=$(echo "$vcenter_data" | jq -r '.vlan_info[]' | sort | sed -n "${vlan_choice}p")

L’utilisateur ne tape plus un nom, il choisit un numéro dans une liste. La faute de frappe devient structurellement impossible.

Le reste du formulaire enchaîne sur ce qui ne peut pas être deviné (combien de VM, quelles specs, quelle IP) et génère le terraform.tfvars. Deux automatismes se sont greffés dessus au fil du temps :

  • Le nom de la VM est calculé, pas saisi. Le script propose un nom conforme à la nomenclature maison, que l’utilisateur peut corriger (read -e -i pré-remplit la saisie, c’est parfait pour ça).
  • Le masque et la passerelle sont déduits du VLAN choisi, via une table de correspondance. Une info de moins à aller chercher, une erreur de moins.

Le tout est emballé dans un script chapeau qui crée le dossier de déploiement, y copie les fichiers Terraform, lance terraform init puis le formulaire. L’utilisateur tape une commande, répond à une dizaine de questions, relit son plan, et applique.

Chaîne de déploiement : deploy.sh crée le dossier de projet, le formulaire interactif est alimenté par l'inventaire réel du vCenter, génère le terraform.tfvars, puis plan et apply créent les VM

La chaîne complète, de la commande au vCenter.

C’est là que c’est devenu un outil, et plus un projet Terraform.

Soyons clairs sur ce que c’était : une v1. Du bash, un peu de Python, aucun état partagé, et les défauts que je détaille plus bas. Mais ça tournait, ça sortait des machines conformes, et c’était mes premiers pas dans l’IaC. On commence rarement par une plateforme parfaite, on commence par le truc qui résout le problème qu’on a sous les yeux.

Le post-déploiement


Les disques supplémentaires créés par Terraform arrivent bruts dans la VM : ni partitionnés, ni formatés, ni montés.

Pour traiter ça : les provisioners. Un file pour pousser un script sur la machine fraîchement déployée, un remote-exec pour l’exécuter. Le script scanne le bus SCSI, ignore les disques déjà partitionnés, et pour chaque disque vierge crée un volume group, un logical volume (LVM fait très bien ce travail), un système de fichiers XFS, le point de montage et l’entrée dans /etc/fstab.

Au passage, ce remote-exec règle un piège du clonage : toutes les VM issues d’un même template héritent des mêmes clés SSH d’hôte. Toute la flotte présente donc la même empreinte, ce qui vide de son sens la vérification d’empreinte. Trois commandes suffisent :

sudo rm -v /etc/ssh/ssh_host_*
sudo dpkg-reconfigure -f noninteractive openssh-server
sudo systemctl restart sshd

Ça marche très bien. Et pourtant, c’est la partie que je referais autrement. Les provisioners sont un anti-pattern reconnu, et HashiCorp le dit elle-même : « provisioners are a last resort ». Ce n’est pas idempotent, ça dépend du SSH, du timing et du réseau, et Terraform n’a aucune idée de ce que le script a fait. Si ça casse au milieu, la VM est dans un état que le state ignore, ce fichier où Terraform note tout ce qu’il a créé pour savoir quoi comparer au passage suivant.

La bonne réponse, c’est de sortir la configuration de Terraform. Terraform crée l’infra, Ansible la configure. C’est la séparation qui tient dans le temps, et sur du Linux c’est direct. Sur du Windows, il faut passer par du PowerShell et du WinRM, exécutés pendant ou après le déploiement : je n’ai pas creusé cette piste, mais c’est clairement par là qu’il faut aller plutôt que d’empiler des provisioners.

Windows, parlons-en


Le provider gère Windows, il n’y a pas de débat. Le clone, le computer_name, la config réseau, tout passe par windows_options sans difficulté.

Mais dès qu’on sort de ce périmètre, ça devient laborieux. J’avais commencé à coder la jonction automatique au domaine Active Directory : choix du domaine, saisie du compte, chemin de l’OU. J’ai fini par l’abandonner. Trop lourd, trop de cas particuliers…

C’est faisable, hein. Mais il ne faut pas pousser. Sur du Linux, tout ce qui suit le déploiement s’enchaîne naturellement ; sur du Windows, chaque étape supplémentaire coûte trois fois plus cher.

Avec le recul


Le projet a tourné et il a rendu service, mais je ne vais pas le vendre comme un modèle. Ce que je ferais autrement :

  • Le script a fini à près de 600 lignes. L’interface, la logique de nommage et les données de configuration cohabitaient dans le même fichier. Les tables de VLAN, de passerelles et de préfixes auraient dû vivre dans un JSON à côté. Pour ajouter un VLAN, il fallait éditer du bash et espérer ne rien casser.
  • Un dossier horodaté par déploiement. Pratique sur le moment, ingérable ensuite : six mois plus tard, retrouver quel dossier correspond à quelle machine/quel projet pour faire un destroy propre relève de l’archéologie.
  • Les provisioners, déjà évoqués. Ansible aurait fait le travail proprement.

Un point que je ne regrette pas en revanche : avoir figé le choix du template. Au début, le formulaire proposait tous les templates trouvés sur le vCenter. En pratique il n’y en avait qu’un seul à jour par OS, et laisser le choix ouvert revenait à offrir la possibilité de déployer sur un template périmé. Une question en moins, une erreur en moins.

Ce n’était qu’un point de départ


Parce qu’il faut être honnête sur ce que ça représente : déployer des VM proprement, c’est la première marche.

  • Du Git pour tout versionner. Le code Terraform, le script, les playbooks, et surtout la configuration de chaque déploiement. Tant que le tfvars est régénéré et jeté à chaque exécution, il n’y a aucune trace de qui a déployé quoi, quand, avec quels paramètres.
  • Ansible derrière, pour tout ce qui suit la création de la machine, à la place des provisioners.
  • Un Vault pour les secrets. La clé SSH de déploiement et les identifiants vCenter n’ont rien à faire dans un fichier ni dans un export de shell. C’est le point le plus urgent de la liste.
  • De la CI pour dérouler le plan puis le apply depuis un pipeline, avec une validation humaine entre les deux, plutôt que depuis le shell de quelqu’un.
  • Un dépôt d’artefacts interne pour distribuer les binaires et les providers sans dépendre d’un accès Internet sortant.

Rien de tout ça n’a été monté, le projet s’est arrêté avant. Mais c’est le bon chemin, et si je devais le reprendre aujourd’hui, je commencerais par le Vault et le versioning.

L’interface web qui n’a jamais vu le jour

Et tout en haut de cette pile, j’avais encore une autre idée : une interface web qui centralise l’ensemble. Un point d’entrée unique pour créer un projet complet, avec des cases à cocher plutôt qu’un terminal. On choisit ce qu’on veut, et la chaîne se déroule toute seule :

  • Terraform crée les machines
  • Ansible les configure et installe ce qui a été coché
  • Les machines sont déclarées dans l’outil de supervision
  • L’inventaire et l’outil de suivi des services sont mis à jour dans la foulée
  • Le state Terraform part dans un backend distant, au lieu de vivre sur le poste de celui qui a lancé la commande

L’intérêt n’est pas technique, il est humain : rendre le déploiement accessible à quelqu’un qui ne connaît ni Git, ni Terraform, ni Ansible, ni Linux. Une CI ferait une bonne partie de l’orchestration, c’est vrai, mais elle ne répond pas à cette question-là, puisqu’il faut toujours savoir écrire un fichier et pousser un commit. L’interface, c’est ce qui fait passer d’un outil d’équipe technique à un service que tout le monde peut utiliser.

J’avais commencé à la monter, et honnêtement ça commençait à ressembler à quelque chose. Ça n’a jamais abouti. Faute de temps, mais surtout parce que Terraform lui-même n’intéressait pas la hiérarchie : construire une interface au-dessus d’un outil que personne d’autre n’utilisait, ça n’avait plus tellement de sens. Cela dit, avec ce que permet le vibe coding aujourd’hui, ce serait largement à portée, et sans doute bien plus propre que ce que j’aurais bricolé à l’époque.

Conclusion


Si vous êtes sur du VMware et que vous vous demandez si l’Infrastructure as Code est réservée au cloud public : non. Faire de l’IaC sur du on-premise est tout à fait réalisable, contrairement à ce qu’on pourrait croire. Le provider vSphere est mature, il fait le boulot, et il transforme une tâche répétitive et propice aux erreurs en quelque chose de reproductible.

Le plus intéressant, c’est que la partie difficile n’a pas été Terraform. La syntaxe HCL s’apprend en quelques jours, et avec l’IA c’est encore plus rapide aujourd’hui. Attention quand même à ne pas tout lui faire faire : relisez ce qu’elle produit et apprenez en même temps, c’est comme ça que j’ai procédé personnellement. Le vrai travail, c’est de comprendre son propre contexte assez bien pour le transformer en variables, et d’emballer le tout dans quelque chose de fiable. Après quoi, il reste la satisfaction de déployer un projet entier en quelques minutes.

Reste la question de l’adoption, et je ne vais pas faire semblant : construire l’outil est une chose, le faire entrer dans les habitudes en est une autre. Ça demande de savoir le vendre, de trouver les bons arguments, et surtout d’avoir des gens réceptifs en face. Les deux ne sont pas toujours réunis 😉

Mais en 2026, pour moi, l’IaC n’est plus une option. Ne pas s’y intéresser quand on gère de l’infrastructure, c’est une erreur. Et si votre parc est sur vSphere, vous n’avez aucune excuse pour attendre d’avoir du cloud public pour vous y mettre.

Et si vous êtes plutôt sur du Proxmox, sachez qu’il existe aussi un provider, très bien documenté par Stéphane Robert. J’avais commencé à m’y mettre sur mon homelab, sans avoir le temps de finir : entre-temps, j’avais branché Claude sur Telegram pour piloter mon infra, et ça a un peu changé mes priorités.

Liens utiles :

Cheatsheets liées