
Terraform crée l’infrastructure, Ansible la configure. En combinant les deux, vous déployez une VM et installez vos logiciels en un seul workflow, sans inventaire statique à maintenir. Ce guide vous montre comment déclarer vos serveurs dans le code Terraform et laisser Ansible les découvrir automatiquement grâce à l’inventaire dynamique.
La séparation entre provisionnement et configuration est un pattern fondamental en DevOps : Terraform gère le “quoi” (quelles machines, quels réseaux), Ansible gère le “comment” (quels paquets, quelle configuration). Le provider ansible fait le lien entre les deux outils sans fichier intermédiaire, sans script de glue.
Prérequis : Terraform ≥ 1.11 installé, KVM/libvirt opérationnel, Ansible installé avec la collection cloud.terraform.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Déclarer un inventaire Ansible dans Terraform : groupes et hôtes via le provider
ansible/ansible - Configurer l’inventaire dynamique : le plugin
cloud.terraform.terraform_providerlit le state Terraform - Enchaîner Terraform et Ansible : créer la VM, puis exécuter un playbook qui l’installe automatiquement
- Appliquer le pattern complet : VM + cloud-init + réseau + provisionnement logiciel
Pourquoi combiner Terraform et Ansible ?
Section intitulée « Pourquoi combiner Terraform et Ansible ? »Quand vous créez une VM avec Terraform, elle démarre avec un système vierge. Cloud-init peut installer quelques paquets au premier boot, mais ses capacités restent limitées : pas de gestion de templates, pas de handlers, pas d’idempotence avancée.
Ansible excelle précisément là où cloud-init s’arrête : configuration fine des services, déploiement d’applications, gestion des redémarrages conditionnels. Le problème classique : comment Ansible sait-il quelles machines cibler ?
Traditionnellement, il fallait maintenir un fichier d’inventaire statique (hosts.ini) à la main, ou écrire un script qui interroge l’hyperviseur. Le provider ansible élimine cette étape : les groupes et hôtes sont déclarés dans le code Terraform, et un plugin d’inventaire les lit directement depuis le state.
Ce qu’est le provider Ansible
Section intitulée « Ce qu’est le provider Ansible »Le provider ansible/ansible ajoute deux types de ressources à Terraform :
| Ressource | Rôle |
|---|---|
ansible_group | Déclare un groupe d’inventaire (ex : webservers, databases) |
ansible_host | Déclare un hôte avec ses variables de connexion (IP, utilisateur, clé SSH) |
Ces ressources ne créent rien sur l’infrastructure : elles enregistrent des informations dans le state Terraform. Le plugin d’inventaire cloud.terraform.terraform_provider lit ensuite ce state pour générer un inventaire Ansible complet.
Installer les prérequis
Section intitulée « Installer les prérequis »Avant de commencer, vérifiez que la collection Ansible cloud.terraform est installée. C’est elle qui fournit le plugin d’inventaire dynamique :
ansible-galaxy collection install cloud.terraformVérification :
ansible-galaxy collection list | grep cloud.terraformLe résultat doit afficher la collection avec sa version (≥ 4.0.0).
Préparer le projet
Section intitulée « Préparer le projet »Créez un répertoire dédié avec la structure suivante :
~/terraform-ansible/├── versions.tf # Providers requis├── variables.tf # Variables d'entrée├── cloud-init.cfg # Configuration premier boot├── network-config.cfg # Réseau statique pour cloud-init├── main.tf # VM + inventaire Ansible├── outputs.tf # Sorties├── inventory.yml # Plugin inventaire dynamique└── playbook.yml # Playbook AnsibleChaque fichier a un rôle précis. Les sections suivantes les détaillent un par un.
Déclarer les providers
Section intitulée « Déclarer les providers »Le fichier versions.tf déclare les deux providers nécessaires : libvirt pour créer la VM et ansible pour gérer l’inventaire :
terraform { required_version = ">= 1.11.0" required_providers { libvirt = { source = "dmacvicar/libvirt" version = "~> 0.8" } ansible = { source = "ansible/ansible" version = "~> 1.3" } }}
provider "libvirt" { uri = "qemu:///system"}Le provider ansible n’a pas besoin de bloc provider {} : il ne se connecte à aucun service externe. Il écrit uniquement dans le state local.
Définir les variables
Section intitulée « Définir les variables »Le fichier variables.tf centralise les paramètres modifiables. Adaptez base_image au chemin de votre image Ubuntu cloud :
variable "vm_name" { description = "Nom de la VM" type = string default = "ansible-vm"}
variable "base_image" { description = "Chemin de l'image Ubuntu cloud (qcow2)" type = string default = "/chemin/vers/ubuntu-24.04-cloudimg.img"}
variable "ssh_public_key" { description = "Chemin de la clé publique SSH" type = string default = "~/.ssh/id_ed25519.pub"}
variable "ssh_private_key" { description = "Chemin de la clé privée SSH" type = string default = "~/.ssh/id_ed25519"}Configurer cloud-init
Section intitulée « Configurer cloud-init »Cloud-init s’exécute au premier démarrage de la VM. Le fichier cloud-init.cfg crée un utilisateur avec votre clé SSH et installe le qemu-guest-agent (nécessaire pour que libvirt détecte l’IP de la VM) :
#cloud-configusers: - name: ubuntu sudo: ALL=(ALL) NOPASSWD:ALL groups: sudo shell: /bin/bash ssh_authorized_keys: - ${ssh_public_key}package_update: truepackages: - qemu-guest-agentruncmd: - systemctl enable --now qemu-guest-agentLa variable ${ssh_public_key} sera remplacée par Terraform grâce à la fonction templatefile().
Le fichier network-config.cfg configure une IP statique via netplan (format cloud-init v2). Adaptez l’adresse à votre réseau libvirt :
version: 2ethernets: enp1s0: dhcp4: false addresses: - 192.168.122.200/24 routes: - to: default via: 192.168.122.1 nameservers: addresses: - 192.168.122.1 - 8.8.8.8Écrire le fichier principal
Section intitulée « Écrire le fichier principal »Le fichier main.tf contient quatre types de ressources. Voici la logique avant le code :
- Un volume disque : copie de l’image Ubuntu dans le pool libvirt
- Une ISO cloud-init : contient la configuration utilisateur + réseau
- Une VM : assemblage du disque, du cloud-init et du réseau
- Un inventaire Ansible : groupe
webservers+ hôte avec les variables de connexion
# ── Volume disque ─────────────────────────────────────resource "libvirt_volume" "disk" { name = "${var.vm_name}.qcow2" # Nom du fichier dans le pool pool = "default" # Pool de stockage libvirt target = { format = { type = "qcow2" } } create = { content = { url = var.base_image } } # Copie l'image de base}
# ── Cloud-init ISO ────────────────────────────────────resource "libvirt_cloudinit_disk" "init" { name = "${var.vm_name}-cloud-init.iso"
# Configuration utilisateur (clé SSH, paquets) user_data = templatefile("${path.module}/cloud-init.cfg", { ssh_public_key = trimspace(file(pathexpand(var.ssh_public_key))) })
# Configuration réseau (IP statique) network_config = file("${path.module}/network-config.cfg")
# Métadonnées obligatoires meta_data = jsonencode({ instance-id = var.vm_name local-hostname = var.vm_name })}
# ── VM ────────────────────────────────────────────────resource "libvirt_domain" "vm" { name = var.vm_name type = "kvm" memory = 1024 # 1 Go de RAM memory_unit = "MiB" vcpu = 1 running = true # Démarrer la VM immédiatement
os = { type = "hvm", type_arch = "x86_64", type_machine = "q35" }
devices = { disks = [ { driver = { name = "qemu", type = "qcow2" } # Format du disque source = { file = { file = libvirt_volume.disk.path } } target = { dev = "vda", bus = "virtio" } # Disque principal }, { device = "cdrom" # Cloud-init en CDROM driver = { name = "qemu", type = "raw" } source = { file = { file = libvirt_cloudinit_disk.init.path } } target = { dev = "sda", bus = "sata" } read_only = true }, ] interfaces = [{ model = { type = "virtio" } source = { network = { network = "default" } } # Réseau libvirt }] }}
# ── Inventaire Ansible ────────────────────────────────resource "ansible_group" "webservers" { name = "webservers" # Nom du groupe dans l'inventaire}
resource "ansible_host" "vm" { name = var.vm_name # Nom de l'hôte groups = [ansible_group.webservers.name] # Appartenance au groupe
variables = { ansible_host = "192.168.122.200" # IP de la VM ansible_user = "ubuntu" # Utilisateur SSH ansible_ssh_private_key_file = pathexpand(var.ssh_private_key) ansible_ssh_common_args = "-o StrictHostKeyChecking=no" }}Ajouter les outputs
Section intitulée « Ajouter les outputs »Le fichier outputs.tf affiche les informations utiles après le terraform apply :
output "vm_name" { description = "Nom de la VM" value = libvirt_domain.vm.name}
output "ansible_group" { description = "Groupe Ansible" value = ansible_group.webservers.name}
output "ansible_host" { description = "Hôte Ansible" value = ansible_host.vm.name}Configurer l’inventaire dynamique
Section intitulée « Configurer l’inventaire dynamique »Le fichier inventory.yml est le point d’entrée d’Ansible. Une seule ligne suffit pour activer le plugin d’inventaire :
---plugin: cloud.terraform.terraform_providerCe plugin lit le fichier terraform.tfstate dans le répertoire courant et génère automatiquement un inventaire à partir des ressources ansible_group et ansible_host.
Écrire le playbook
Section intitulée « Écrire le playbook »Le playbook playbook.yml installe nginx sur tous les serveurs du groupe webservers et vérifie qu’il répond :
---- name: Configurer le serveur web hosts: webservers become: true tasks: - name: Installer nginx ansible.builtin.apt: update_cache: true name: nginx state: present
- name: Activer et démarrer nginx ansible.builtin.service: name: nginx state: started enabled: true
- name: Vérifier que nginx répond ansible.builtin.uri: url: "http://localhost" status_code: 200 register: result
- name: Afficher le résultat ansible.builtin.debug: msg: "nginx répond avec le code {{ result.status }}"Déployer et configurer
Section intitulée « Déployer et configurer »-
Initialisez Terraform pour télécharger les providers libvirt et ansible :
Fenêtre de terminal cd ~/terraform-ansibleterraform init -
Validez la configuration pour détecter les erreurs de syntaxe :
Fenêtre de terminal terraform validateLe résultat attendu est
Success! The configuration is valid. -
Appliquez pour créer la VM et enregistrer l’inventaire :
Fenêtre de terminal terraform apply -auto-approveTerraform crée 5 ressources :
Apply complete! Resources: 5 added, 0 changed, 0 destroyed.Outputs:ansible_group = "webservers"ansible_host = "ansible-vm"vm_name = "ansible-vm" -
Attendez que la VM soit prête (cloud-init prend 30 à 60 secondes) :
Lancez ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_ed25519 ubuntu@192.168.122.200 "hostname".
Quand SSH répond ansible-vm, la VM est prête.
-
Vérifiez l’inventaire dynamique généré par Terraform :
Fenêtre de terminal ansible-inventory -i inventory.yml --listLe résultat montre le groupe
webserversavec l’hôteansible-vmet ses variables de connexion :{"webservers": {"hosts": ["ansible-vm"]},"_meta": {"hostvars": {"ansible-vm": {"ansible_host": "192.168.122.200","ansible_user": "ubuntu","ansible_ssh_private_key_file": "/home/user/.ssh/id_ed25519","ansible_ssh_common_args": "-o StrictHostKeyChecking=no"}}}} -
Exécutez le playbook pour installer nginx :
Fenêtre de terminal ansible-playbook playbook.yml -i inventory.ymlLe résultat attendu :
PLAY [Configurer le serveur web] *********************************************TASK [Gathering Facts] *******************************************************ok: [ansible-vm]TASK [Installer nginx] *******************************************************changed: [ansible-vm]TASK [Activer et démarrer nginx] *********************************************ok: [ansible-vm]TASK [Vérifier que nginx répond] *********************************************ok: [ansible-vm]TASK [Afficher le résultat] **************************************************ok: [ansible-vm] => {"msg": "nginx répond avec le code 200"}PLAY RECAP *******************************************************************ansible-vm : ok=5 changed=1 unreachable=0 failed=0
Comment ça fonctionne ensemble
Section intitulée « Comment ça fonctionne ensemble »Le diagramme ci-dessous résume le flux de données entre Terraform et Ansible :
terraform apply │ ├──→ libvirt_volume ──→ disque qcow2 ├──→ libvirt_cloudinit ──→ ISO cloud-init ├──→ libvirt_domain ──→ VM démarrée ├──→ ansible_group ──→ state (groupe "webservers") └──→ ansible_host ──→ state (hôte "ansible-vm") │ ▼ terraform.tfstate │ ▼ inventory.yml (plugin) │ ▼ ansible-playbook │ ▼ nginx installé sur la VMLes ressources ansible_group et ansible_host n’existent que dans le state — elles ne créent rien sur l’infrastructure. Le plugin d’inventaire cloud.terraform.terraform_provider lit ce state et génère un inventaire Ansible standard.
Dépannage
Section intitulée « Dépannage »| Symptôme | Cause probable | Solution |
|---|---|---|
ssh: connect to host ... Connection refused | Cloud-init n’a pas fini | Attendre 30-60 secondes, réessayer |
ansible-inventory ne trouve aucun hôte | Pas de terraform.tfstate dans le répertoire | Vérifier que terraform apply a été exécuté dans le même répertoire |
No module named 'cloud.terraform' | Collection Ansible manquante | ansible-galaxy collection install cloud.terraform |
Playbook échoue sur apt update | Pas de réseau dans la VM | Vérifier network-config.cfg et l’IP gateway |
UNREACHABLE dans Ansible | Mauvaise clé SSH ou mauvais user | Vérifier ssh_public_key dans variables.tf et ansible_user dans main.tf |
Nettoyage
Section intitulée « Nettoyage »Pour supprimer l’intégralité des ressources créées :
terraform destroy -auto-approveTerraform détruit les 5 ressources dans l’ordre inverse de création. La VM, le disque, l’ISO cloud-init et les entrées d’inventaire sont supprimés.
À retenir
Section intitulée « À retenir »- Le provider
ansible/ansibleajoute des ressourcesansible_groupetansible_hostau state Terraform — il ne crée rien sur l’infrastructure - Le plugin
cloud.terraform.terraform_providergénère l’inventaire Ansible en lisant le state, sans fichier statique - Cloud-init configure la VM au premier boot (utilisateur SSH, paquets de base, réseau)
- Le workflow standard est :
terraform apply→ attendre SSH →ansible-playbook - L’IP de la VM est définie dans
network-config.cfget dansansible_host.variables— elles doivent correspondre - Ce pattern sépare le provisionnement (Terraform) de la configuration (Ansible), chaque outil fait ce qu’il sait le mieux