Un homelab devient rapidement difficile à maintenir dès que le nombre de VM augmente, que plusieurs distributions coexistent ou que les tests doivent être répétés. Créer une machine dans l’interface Proxmox ou vCenter, modifier sa configuration à la main, puis installer les paquets via SSH fonctionne pour un essai isolé. Cette méthode ne tient plus lorsque l’objectif est de reconstruire un environnement complet après une erreur, une migration de stockage ou un changement de matériel.

L’approche Infrastructure as Code (IaC) sépare généralement deux responsabilités. Terraform décrit et crée les ressources d’infrastructure : VM, réseaux, disques, tags ou pools de ressources. Ansible configure ensuite les systèmes invités : comptes, paquets, services, fichiers, règles de pare-feu et applications. Terraform déclare l’existence de la machine ; Ansible déclare son état logiciel.

Cette séparation est particulièrement utile dans un homelab. Elle permet de détruire un cluster de test, de le redéployer avec les mêmes paramètres, puis d’appliquer une configuration identique sans dépendre d’une série de manipulations manuelles difficilement traçables.

Architecture IaC typique pour un homelab

Une architecture simple et efficace repose sur quatre composants :

Dans un premier temps, il est préférable de conserver une structure volontairement modeste :

homelab-iac/
├── terraform/
│   ├── main.tf
│   ├── variables.tf
│   ├── terraform.tfvars.example
│   └── outputs.tf
├── ansible/
│   ├── inventory/
│   │   └── hosts.yml
│   ├── group_vars/
│   │   └── all.yml
│   ├── playbooks/
│   │   └── baseline.yml
│   └── roles/
│       └── common/
├── scripts/
│   └── generate-inventory.sh
├── .gitignore
└── README.md

Terraform conserve l’état des ressources créées dans un fichier terraform.tfstate. Pour un homelab personnel, un état local peut être acceptable, à condition de le sauvegarder. Dès que plusieurs personnes interviennent ou qu’une automatisation CI est ajoutée, il faut envisager un backend distant avec verrouillage, par exemple S3 compatible, GitLab HTTP backend ou Terraform Cloud.

Ansible n’a pas de fichier d’état comparable. Son fonctionnement est idempotent : un playbook doit pouvoir être relancé sans provoquer de modifications inutiles. L’état attendu est décrit dans les tâches, puis Ansible compare cette intention avec la machine cible.

Le point de jonction entre les deux outils est l’inventaire. Terraform connaît les adresses IP ou les noms DNS des VM qu’il crée. Ansible doit ensuite les utiliser comme cibles. Dans un environnement simple, un inventaire statique suffit. Dans un environnement plus automatisé, Terraform peut produire un fichier d’inventaire YAML ou JSON à partir de ses outputs. Pour aller plus loin, consultez notre guide de construction d’un homelab.

Préparer les images et les accès

L’automatisation ne compense pas une image de base incohérente. Avant de créer des VM avec Terraform, préparez un template propre pour chaque famille de système utilisée. Sous Proxmox, il s’agit souvent d’un template clonable depuis une image cloud Debian, Ubuntu, Rocky Linux ou AlmaLinux. Sous vSphere, ce sera une VM template ou une image gérée avec Content Library.

Pour les distributions Linux compatibles cloud-init, l’image doit idéalement inclure :

Le guest agent est important. Il permet à l’hyperviseur de remonter l’adresse IP, l’état de la VM et parfois des informations complémentaires. Sans lui, Terraform peut créer correctement la machine tout en étant incapable d’attendre de manière fiable qu’elle soit joignable.

Les secrets ne doivent jamais être intégrés directement au dépôt Git. Les points sensibles habituels sont :

Une pratique minimale consiste à utiliser des variables d’environnement pour les identifiants Terraform et Ansible Vault pour les variables Ansible chiffrées. Pour un homelab plus mature, un gestionnaire tel que Vault, Bitwarden Secrets Manager ou une instance SOPS avec chiffrement par âge peut remplacer ces mécanismes.

Créer des VM déclaratives avec Terraform et Proxmox

Les providers Proxmox ne sont pas tous équivalents. Plusieurs implémentations communautaires existent, avec des modèles de ressources et des cycles de publication différents. Il faut donc lire la documentation du provider retenu, verrouiller sa version et tester les mises à jour hors production. L’exemple suivant illustre le principe avec le provider bpg/proxmox, utilisé avec des VM clonées depuis un template cloud-init.

Un playbook Ansible appliqué à un homelab de virtualisation.
Un playbook Ansible appliqué à un homelab de virtualisation.

Le fichier main.tf décrit ici trois noeuds de lab : un serveur d’automatisation, un noeud applicatif et un noeud de test.

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

provider "proxmox" {
  endpoint  = var.proxmox_endpoint
  api_token = var.proxmox_api_token

  # A n'activer qu'en homelab si le certificat est auto-signé.
  insecure = var.proxmox_insecure
}

resource "proxmox_virtual_environment_vm" "lab" {
  for_each = var.vms

  name      = each.key
  node_name = var.proxmox_node
  vm_id     = each.value.vm_id

  clone {
    vm_id = var.template_vm_id
    full  = true
  }

  cpu {
    cores = each.value.cpu
    type  = "host"
  }

  memory {
    dedicated = each.value.memory_mb
  }

  disk {
    datastore_id = var.datastore_id
    interface    = "scsi0"
    size         = each.value.disk_gb
  }

  network_device {
    bridge = var.network_bridge
  }

  initialization {
    ip_config {
      ipv4 {
        address = each.value.ip_cidr
        gateway = var.gateway_ipv4
      }
    }

    user_account {
      username = var.ssh_user
      keys     = [trimspace(file(var.ssh_public_key_path))]
    }

    dns {
      servers = var.dns_servers
    }
  }

  agent {
    enabled = true
  }

  tags = ["homelab", "terraform", each.value.role]
}

La variable vms évite de dupliquer les blocs de ressources. Dans terraform.tfvars, elle peut être définie ainsi :

proxmox_endpoint  = "https://pve.lab.example:8006/api2/json"
proxmox_node      = "pve01"
template_vm_id    = 9000
datastore_id      = "local-lvm"
network_bridge    = "vmbr0"
gateway_ipv4      = "192.168.50.1"
dns_servers       = ["192.168.50.10", "1.1.1.1"]
ssh_user          = "automation"
ssh_public_key_path = "~/.ssh/homelab_ed25519.pub"

vms = {
  ansible01 = {
    vm_id     = 101
    role      = "automation"
    cpu       = 2
    memory_mb = 4096
    disk_gb   = 32
    ip_cidr   = "192.168.50.21/24"
  }

  app01 = {
    vm_id     = 110
    role      = "application"
    cpu       = 2
    memory_mb = 2048
    disk_gb   = 20
    ip_cidr   = "192.168.50.31/24"
  }

  test01 = {
    vm_id     = 111
    role      = "test"
    cpu       = 2
    memory_mb = 2048
    disk_gb   = 20
    ip_cidr   = "192.168.50.32/24"
  }
}

L’exécution suit le cycle Terraform habituel :

cd terraform
terraform init
terraform fmt -recursive
terraform validate
terraform plan -out=tfplan
terraform apply tfplan

Le fichier terraform.tfvars contient potentiellement des informations sensibles. Il ne doit pas être commité. Une bonne pratique consiste à versionner terraform.tfvars.example, sans jeton API ni données spécifiques au réseau domestique.

L’adresse IP statique dans cet exemple simplifie l’intégration avec Ansible. Une autre approche consiste à distribuer les baux via DHCP et à récupérer les adresses grâce au guest agent, aux enregistrements DNS ou à une source d’inventaire dynamique. Pour un homelab, les IP statiques déclarées sont souvent plus prévisibles et plus faciles à diagnostiquer.

Configurer les VM avec Ansible

Une fois les VM créées et démarrées, Ansible prend le relais. L’objectif du premier playbook n’est pas d’installer immédiatement une pile Kubernetes, un serveur Git ou une plateforme de supervision. Il faut d’abord appliquer un socle commun : mises à jour, outils de diagnostic, SSH, fuseau horaire, agent invité et règles de sécurité de base. Pour aller plus loin, consultez l’automatisation côté VMware avec PowerCLI.

Terraform provisionne l'infrastructure de façon déclarative.
Terraform provisionne l'infrastructure de façon déclarative.

Voici un inventaire statique minimal, inventory/hosts.yml :

all:
  vars:
    ansible_user: automation
    ansible_ssh_private_key_file: ~/.ssh/homelab_ed25519
    ansible_python_interpreter: /usr/bin/python3

  children:
    automation:
      hosts:
        ansible01:
          ansible_host: 192.168.50.21

    application:
      hosts:
        app01:
          ansible_host: 192.168.50.31

    test:
      hosts:
        test01:
          ansible_host: 192.168.50.32

Le playbook suivant applique une configuration commune aux systèmes Debian et Ubuntu. Il illustre l’idempotence : les paquets sont déclarés présents, les services démarrés et activés, les fichiers rendus avec un contenu contrôlé.

---
- name: Appliquer le socle Linux du homelab
  hosts: all
  become: true
  gather_facts: true

  vars:
    baseline_packages:
      - qemu-guest-agent
      - curl
      - git
      - vim
      - htop
      - ca-certificates
      - unattended-upgrades

  tasks:
    - name: Mettre à jour le cache APT
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600

    - name: Installer les paquets de base
      ansible.builtin.apt:
        name: "{{ baseline_packages }}"
        state: present

    - name: Définir le fuseau horaire
      community.general.timezone:
        name: Europe/Paris

    - name: Activer et démarrer QEMU Guest Agent
      ansible.builtin.service:
        name: qemu-guest-agent
        state: started
        enabled: true

    - name: Désactiver l'authentification SSH par mot de passe
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^#?PasswordAuthentication'
        line: 'PasswordAuthentication no'
        validate: '/usr/sbin/sshd -t -f %s'
      notify: Redémarrer SSH

    - name: Créer le répertoire de bannières
      ansible.builtin.file:
        path: /etc/issue.d
        state: directory
        mode: '0755'

    - name: Ajouter une bannière de gestion
      ansible.builtin.copy:
        dest: /etc/issue.d/homelab.issue
        content: |
          Système géré par Ansible.
          Les modifications manuelles doivent être documentées.
        mode: '0644'

  handlers:
    - name: Redémarrer SSH
      ansible.builtin.service:
        name: ssh
        state: restarted

Exécution :

cd ansible
ansible-playbook -i inventory/hosts.yml playbooks/baseline.yml

Avant de lancer un playbook pour la première fois, un contrôle de connectivité est utile :

ansible -i inventory/hosts.yml all -m ping

Dans un environnement réel, le playbook de base évoluera vers des rôles : common, docker, monitoring-agent, k3s-node, backup-client, etc. Les rôles évitent que les playbooks deviennent des fichiers monolithiques et favorisent la réutilisation entre plusieurs projets de lab.

Chaîner Terraform et Ansible sans couplage excessif

Il est tentant d’appeler Ansible directement depuis Terraform avec des provisioners local-exec ou remote-exec. Cette méthode fonctionne, mais elle crée un couplage fragile entre les deux outils. Terraform doit principalement gérer le cycle de vie des ressources ; Ansible doit gérer la configuration des systèmes.

Une chaîne simple et robuste est la suivante :

  1. terraform apply crée ou met à jour les VM ;
  2. Terraform expose les IP et rôles avec des outputs ;
  3. un script génère ou met à jour l’inventaire Ansible ;
  4. ansible-playbook applique la configuration ;
  5. des tests de validation contrôlent le résultat.

Exemple d’output Terraform :

output "vm_addresses" {
  value = {
    for name, vm in proxmox_virtual_environment_vm.lab :
    name => {
      role = var.vms[name].role
      ip   = var.vms[name].ip_cidr
    }
  }
}

Pour une intégration plus propre, le script de génération peut consommer terraform output -json, supprimer le suffixe CIDR et construire un inventaire YAML. Si le lab utilise déjà un DNS interne fiable, il peut aussi produire des noms d’hôtes au lieu d’adresses IP.

Cette séparation permet de rejouer Ansible sans toucher à l’infrastructure. Elle permet aussi de modifier les ressources Terraform, par exemple en ajoutant de la mémoire à une VM, sans réinstaller toute la configuration applicative.

Reproductibilité, limites et discipline d’exploitation

Le premier bénéfice est la reproductibilité. Une VM supprimée par erreur peut être recréée avec le même nombre de vCPU, la même mémoire, le même réseau, les mêmes tags et la même configuration logicielle. Cela transforme le homelab en environnement jetable, ce qui est précisément l’un de ses intérêts pédagogiques. Pour aller plus loin, consultez le choix d’hyperviseur en homelab.

Le deuxième bénéfice est la traçabilité. Les changements passent par Git, donc il devient possible de répondre à des questions simples mais importantes : quand une VM a-t-elle été ajoutée, pourquoi le réseau a-t-il changé, quelle version d’un rôle Ansible a introduit un paquet, quelles valeurs étaient prévues pour le cluster de test ?

Le troisième bénéfice est la réduction de la dérive de configuration. Sans automatisation, deux VM censées être identiques finissent généralement par diverger : paquet installé à la main, fichier modifié lors d’un dépannage, mise à jour non appliquée, configuration SSH particulière. Ansible ne supprime pas toute dérive, mais il fournit un moyen concret de réimposer l’état attendu.

Il faut néanmoins connaître les limites :

Une discipline utile consiste à réserver l’interface Proxmox ou vCenter au diagnostic et aux opérations d’urgence. Toute modification durable doit ensuite être reportée dans Terraform ou Ansible. Sinon, le code cesse progressivement de représenter l’état réel.

Enfin, documentez les dépendances externes : VLAN, réservation DHCP, DNS, stockage, certificats, dépôts APT et accès Internet. L’IaC rend l’environnement reproductible seulement si les prérequis autour de l’hyperviseur sont eux aussi connus et stables.