Aller au contenu
Infrastructure as Code medium

Préparer le lab Ansible sur KVM/libvirt en moins de 10 minutes

18 min de lecture

Logo Ansible

Vous avez besoin d’un terrain de jeu réaliste pour apprendre Ansible : pas une seule VM jouet, mais une vraie topologie multi-hôtes que vous pouvez détruire et recréer à volonté. Ce guide monte en moins de 10 minutes la topologie pédagogique de référence du parcours RHCE — 1 control node + 3 managed nodes AlmaLinux 10 sur KVM/libvirt, avec cloud-init et baux DHCP statiques. Tous les guides suivants s’appuient sur cette infrastructure. L’approche est minimale et reproductible : virt-install + cloud-init, scriptés via un Makefile unique. Pas de Terraform pour démarrer, pas d’agent à installer.

  • Créer un réseau libvirt isolé avec NAT et baux DHCP statiques sans toucher à NetworkManager côté VMs ;
  • Provisionner 4 VMs AlmaLinux 10 via virt-install avec une image cloud mutualisée ;
  • Pré-configurer chaque VM via cloud-init : utilisateur ansible, clé SSH, sudo NOPASSWD ;
  • Vérifier que les 4 VMs sont accessibles en SSH avec une seule commande Make ;
  • Naviguer entre les 103 labs avec la CLI dsoxlab (suivi de progression SQLite local, README rendus en terminal, suggestion du prochain lab) ;
  • Détruire proprement le lab pour le recréer rapidement.

make bootstrap installe tout l’outillage logiciel nécessaire : libvirt, virt-install, cloud-utils, wget, pipx, ansible, ansible-lint, pytest, testinfra. Vous n’avez pas à installer ces paquets manuellement — le script scripts/bootstrap.sh détecte votre distribution (Fedora/RHEL/AlmaLinux/Ubuntu/Debian/Arch) et lance les bonnes commandes.

Côté matériel et système, vérifiez seulement :

ÉlémentComment vérifier
Au moins 8 Go de RAM librefree -h
Au moins 40 Go d’espace disque dans /var/lib/libvirt/images/df -h /var/lib/libvirt/images
Une paire de clés SSH (Ed25519 recommandé)ls ~/.ssh/id_ed25519* (sinon make bootstrap la génère sous ssh/id_ed25519)
Distribution Linux avec KVM activé dans le BIOSegrep -c '(vmx|svm)' /proc/cpuinfo retourne ≥ 1

Le lab pédagogique reprend la topologie minimale qui couvre 80 % des objectifs RHCE EX294 sans sur-dimensionner :

HôteIPMAC (fixe)RAMvCPUDisqueRôle
control-node.lab10.10.20.1052:54:00:ab:00:102 GiB220 GiBPoste Ansible
web1.lab10.10.20.2152:54:00:ab:00:211 GiB110 GiBWeb (groupe webservers)
web2.lab10.10.20.2252:54:00:ab:00:221 GiB110 GiBWeb (groupe webservers)
db1.lab10.10.20.3152:54:00:ab:00:311.5 GiB115 GiBDB (groupe dbservers)

Le réseau libvirt lab-ansible utilise le sous-réseau 10.10.20.0/24 avec :

  • NAT sortant pour que les VMs accèdent à Internet (mise à jour des paquets, dnf install) ;
  • Plage DHCP dynamique 10.10.20.100-200 (pour d’éventuels hôtes additionnels) ;
  • 4 baux DHCP statiques mappés par MAC pour les 4 VMs du lab — IPs garanties stables.

Cloud-init minimal : la philosophie « Ansible se prépare lui-même »

Section intitulée « Cloud-init minimal : la philosophie « Ansible se prépare lui-même » »

Le cloud-init des 4 VMs ne fait que trois choses :

  1. Pose le hostname et le FQDN (web1.lab, db1.lab…) ;
  2. Crée un utilisateur ansible avec la clé SSH publique en authorized_keys et sudo NOPASSWD ;
  3. Désactive le compte root et le SSH par mot de passe.

Aucun dnf install, aucun service activé, aucune config sysctl. Tout le reste est fait par Ansible lui-même dans la page suivante (Préparer les nœuds gérés). C’est volontaire : ça matérialise concrètement le principe « Ansible converge la cible vers son état désiré » et accélère le make provision (pas de paquets à télécharger côté cloud-init).

Voici le template managed-node.user-data.tmpl (les __VARIABLES__ sont substituées par le script de provisioning) :

#cloud-config
hostname: __HOSTNAME__
fqdn: __HOSTNAME__.lab
manage_etc_hosts: true
preserve_hostname: false
users:
- name: ansible
groups: [wheel]
sudo: "ALL=(ALL) NOPASSWD:ALL"
shell: /bin/bash
lock_passwd: true
ssh_authorized_keys:
- __SSH_PUBKEY__
disable_root: true
ssh_pwauth: false

Le lab pédagogique du site est hébergé dans le dépôt stephrobert/ansible-training, à cloner par exemple dans ~/Projets/ansible-training/. Voici l’arborescence racine et un aperçu des sections de labs (103 labs au total répartis sur 23 sections) :

  • Répertoireansible-training/
    • Makefile
    • ansible.cfg
    • conftest.py (fixture pytest globale)
    • requirements.txt
    • requirements.yml
    • meta.yml (source de vérité de la liste des labs)
    • Répertoirebin/
      • dsoxlab (CLI suivi de progression)
    • Répertoireinventory/
      • hosts.yml
    • Répertoireinfra/
      • Répertoirevirt-install/ (provision/destroy + cloud-init templates)
    • Répertoireee/ (Execution Environment — image OCI)
    • Répertoiredocs/ (doc interne du lab)
    • Répertoirescripts/ (bootstrap, lint-all, snapshot, restore)
    • Répertoiressh/ (clés SSH locales, gitignored)
    • Répertoirecollected/ (cible des fetch, gitignored)
    • Répertoirelabs/
      • Répertoirebootstrap/prepare-managed-nodes/
      • Répertoiredecouvrir/ (4 labs : declaratif-vs-imperatif, installation-ansible, configuration-ansible, prise-en-main-cli)
      • Répertoirepremiers-pas/ (2 labs : premier-playbook, ansible-vault)
      • Répertoireecrire-code/ (28 labs : plays-et-tasks, handlers, tags, variables-base, jinja2-base, conditions-when, boucles-loop, block-rescue-always…)
      • Répertoiremodules-fichiers/ (copy, file, blockinfile, fetch, archive-unarchive)
      • Répertoiremodules-paquets/ (package, dnf-options)
      • Répertoiremodules-services/ (systemd, cron)
      • Répertoiremodules-utilisateurs/ (user, group, authorized-key, sudoers)
      • Répertoiremodules-rhel/ (firewalld, selinux, sysctl, mount, lvm-storage)
      • Répertoiremodules-reseau/ (get-url, uri)
      • Répertoiremodules-diagnostic/ (stat, find, assert-fail, wait-for-pause)
      • Répertoireinventaires/ (group-vars-host-vars, patterns-hotes, dynamique-kvm)
      • Répertoireroles/ (creer-premier-role, variables-defaults-vars, handlers-meta, argument-specs, consommer-role, dependencies)
      • Répertoiremolecule/ (introduction, installation-config, tdd-cycle, scenarios-multi-distro)
      • Répertoiretests/ (testinfra, tox-multiversion, ansible-lint-production)
      • Répertoireci/ (github-actions, gitlab)
      • Répertoiregalaxy/ (ansible-galaxy-cli, installer-roles, auditer-role-existant, versionner-publier)
      • Répertoirevault/ (introduction, chiffrer-fichier-variable, id-multiples, playbooks-mixtes, dans-roles, integration-hashicorp, integration-passbolt)
      • Répertoireee/ (hello, inspection, builder-custom, ci-pipeline, debug)
      • Répertoiretroubleshooting/ (verbosite, debugger, idempotence-perfs)
      • Répertoirecollections/ (decouvrir, requirements, creer-custom, ci-tests, migration-role)
      • Répertoirepratiques/ (ansible-pull-gitops)
      • Répertoirerhce/ (mock-ex294 — examen blanc 4 h chrono)

Chaque lab labs/<section>/<page>/ est autonome : README.md (tutoriel guidé), Makefile (cible clean pour reset), challenge/README.md (consigne du challenge final) + challenge/tests/test_*.py (validation pytest+testinfra). Le Makefile racine orchestre l’infrastructure — vous n’appelez jamais virt-install directement. La CLI dsoxlab suit votre progression dans une SQLite locale (~/.local/share/dsoxlab/progress.db).

  1. Cloner le dépôt et exécuter le bootstrap.

    Le make bootstrap installe les paquets système requis (libvirt, virt-install, cloud-utils), vérifie les outils Ansible (ansible-core, ansible-lint, ansible-navigator, pytest), installe les collections Galaxy depuis requirements.yml, et génère la paire de clés SSH du lab dans ssh/id_ed25519 :

    Fenêtre de terminal
    git clone https://github.com/stephrobert/ansible-training.git ~/Projets/ansible-training
    cd ~/Projets/ansible-training
    make bootstrap

    La clé du lab est séparée de votre clé personnelle pour pouvoir la jeter avec le lab. Elle est gitignorée — chaque utilisateur génère la sienne au premier make bootstrap.

  2. Lancer le provisioning complet.

    Une seule commande crée le réseau libvirt, télécharge l’image AlmaLinux (mise en cache), crée les 4 VMs et exécute le playbook de préparation :

    Fenêtre de terminal
    make provision

    Le script affiche en couleurs ce qu’il fait :

    [provision] Création du réseau libvirt 'lab-ansible' (10.10.20.0/24) avec baux DHCP statiques...
    [OK] Réseau 'lab-ansible' créé avec 4 baux DHCP statiques
    [OK] Image AlmaLinux 10 déjà présente : /var/lib/libvirt/images/AlmaLinux-10-GenericCloud-latest.x86_64.qcow2
    [provision] === control-node (10.10.20.10, MAC 52:54:00:ab:00:10, 2048 MiB RAM, 2 vCPU, 20 GiB) ===
    [provision] === web1 (10.10.20.21, MAC 52:54:00:ab:00:21, 1024 MiB RAM, 1 vCPU, 10 GiB) ===
    [provision] === web2 (10.10.20.22, MAC 52:54:00:ab:00:22, 1024 MiB RAM, 1 vCPU, 10 GiB) ===
    [provision] === db1 (10.10.20.31, MAC 52:54:00:ab:00:31, 1536 MiB RAM, 1 vCPU, 15 GiB) ===
    [provision] Attente SSH (~30s)...
    SSH OK sur les 4 hôtes
    [OK] Lab prêt — lance 'make verify-conn' ou 'make test-all'
  3. Vérifier l’état des VMs.

    Fenêtre de terminal
    virsh list --all | grep -E 'control-node|web|db'

    Sortie attendue :

    49 control-node running
    50 web1 running
    51 web2 running
    52 db1 running
  4. Vérifier les baux DHCP attribués.

    Fenêtre de terminal
    virsh net-dhcp-leases lab-ansible

    Les 4 IPs doivent correspondre à la topologie attendue :

    Expiry Time MAC address Protocol IP address Hostname
    ------------------------------------------------------------------------------
    2026-04-25 15:08:51 52:54:00:ab:00:10 ipv4 10.10.20.10/24 control-node
    2026-04-25 15:06:39 52:54:00:ab:00:21 ipv4 10.10.20.21/24 web1
    2026-04-25 15:09:16 52:54:00:ab:00:22 ipv4 10.10.20.22/24 web2
    2026-04-25 15:06:43 52:54:00:ab:00:31 ipv4 10.10.20.31/24 db1
  5. Tester la connectivité Ansible.

    Fenêtre de terminal
    make verify-conn

    Vous devez voir 4 pong :

    db1.lab | SUCCESS => {
    "changed": false,
    "ping": "pong"
    }
    control-node.lab | SUCCESS => {
    "changed": false,
    "ping": "pong"
    }
    web2.lab | SUCCESS => {
    "changed": false,
    "ping": "pong"
    }
    web1.lab | SUCCESS => {
    "changed": false,
    "ping": "pong"
    }

    À ce point, le lab est fonctionnel bout en bout : Ansible voit les 4 hôtes, SSH passe, Python 3 répond. Vous pouvez attaquer les guides suivants.

Avec 103 labs répartis sur 23 sections, naviguer manuellement devient vite pénible. Le repo embarque une CLI Python appelée dsoxlab (chemin : bin/dsoxlab) qui répond à trois besoins simultanés :

  • Afficher un README de lab rendu en Markdown riche directement dans le terminal (titre, gras, blocs de code colorés, tableaux), sans ouvrir un navigateur ni un éditeur.
  • Suggérer le prochain lab à attaquer en fonction de votre progression, pour éviter le syndrome « par où je commence ? ».
  • Suivre objectivement les résultats : chaque exécution pytest d’un challenge est inscrite automatiquement dans une SQLite locale, avec un dashboard pour voir où vous en êtes.

Le suivi est automatique et silencieux : un plugin pytest interne (livré dans conftest.py du repo) intercepte chaque run lancé sur un dossier labs/<section>/<page>/challenge/tests/ et inscrit le résultat dans la base. Vous n’avez rien à activer : dès que vous lancez pytest sur un challenge, votre progression se met à jour.

Aucun service externe n’est sollicité — pas de réseau, pas d’authentification, pas de données qui sortent de votre poste. Le suivi est conforme RGPD par construction.

Les résultats sont stockés dans ~/.local/share/dsoxlab/progress.db (fichier SQLite). Cet emplacement suit la spec XDG Base Directory — propre, isolé, sauvegardable, gitignored.

Chaque ligne de la base correspond à un run pytest : timestamp, lab concerné, nombre de tests passed/failed, statut dérivé. La même base sert à plusieurs repos ansible-training clonés (utile si vous avez un fork). Pour utiliser une base alternative (sandbox, formation), exportez ANSIBLE_TRAINING_DB=/tmp/test.db avant pytest.

Pour pouvoir taper dsoxlab depuis n’importe où sans préfixer bin/, créez un lien symbolique dans ~/.local/bin/ (qui doit être dans votre PATH) :

Fenêtre de terminal
mkdir -p ~/.local/bin
ln -sf "$(pwd)/bin/dsoxlab" ~/.local/bin/dsoxlab
# Vérification
which dsoxlab # doit afficher /home/<vous>/.local/bin/dsoxlab
dsoxlab show # plus besoin de bin/dsoxlab

Si ~/.local/bin n’est pas dans votre PATH, ajoutez à ~/.bashrc ou ~/.zshrc :

Fenêtre de terminal
export PATH="$HOME/.local/bin:$PATH"

Si vous gérez plusieurs forks d’ansible-training, utilisez plutôt un wrapper qui pointe vers le bon repo :

cat > ~/.local/bin/dsoxlab <<'EOF'
#!/usr/bin/env bash
exec "$HOME/Projets/ansible-training/bin/dsoxlab" "$@"
EOF
chmod +x ~/.local/bin/dsoxlab
CommandeEffet
dsoxlabTableau de bord par section avec icônes (✅ ⏳ ❌ · ⊘)
dsoxlab nextSuggère le prochain lab à attaquer (selon progression)
dsoxlab statsPourcentage de réussite par section
dsoxlab show --section vaultFiltrer le dashboard sur une section
dsoxlab show --status completedLister uniquement les labs validés
dsoxlab lab decouvrir/installation-ansibleAfficher le README du tutoriel (rendu Markdown)
dsoxlab lab vault/introduction --challengeAfficher le challenge/README.md
dsoxlab lab vault/introduction --bothTutoriel + challenge à la suite
dsoxlab reset --lab vault/introduction -yEffacer la progression d’un lab pour le rejouer
dsoxlab reset --allReset complet (avec confirmation)
dsoxlab export -o progress.jsonExporter la progression en JSON portable

L’option --width 100 force la largeur de rendu (par défaut 80, 0 = largeur du terminal). Le commande dsoxlab lab est l’usage le plus fréquent — elle remplace l’aller-retour vers GitHub ou un éditeur Markdown.

IcôneStatutSignification
completedAu moins un run avec 100 % de tests passed
in_progressRun(s) existant(s) avec une partie passed/failed
failedTous les tests échouent
·not_startedAucun run inscrit en base
skippedRun skippé (challenge non écrit, replay désactivé)

Si vous voulez lancer pytest sans inscrire le résultat (debug, expérimentation) :

Fenêtre de terminal
PROGRESS_DISABLED=1 pytest -v labs/ecrire-code/block-rescue-always/challenge/tests/
Fenêtre de terminal
dsoxlab next # 1. Que faire ensuite ?
dsoxlab lab decouvrir/installation-ansible # 2. Lire le tutoriel
# ... vous écrivez lab.yml et challenge/solution.yml ...
pytest -v labs/decouvrir/installation-ansible/challenge/tests/ # 3. Valider
dsoxlab show # 4. Voir l'avancement global

Les cibles Make équivalentes existent (utiles si vous n’avez pas mis ~/.local/bin dans le PATH) : make dsoxlab, make dsoxlab-next, make dsoxlab-stats, make lab LAB=vault/introduction CHALLENGE=1.

Quand vous voulez repartir d’une base propre — pour rejouer le provisioning de zéro, par exemple — :

Fenêtre de terminal
make destroy

Le script demande confirmation, arrête les 4 VMs, supprime leurs disques et démantèle le réseau libvirt. L’image cloud AlmaLinux est conservée dans /var/lib/libvirt/images/ pour ne pas la retélécharger au prochain make provision.

Pour automatiser dans un script, utilisez :

Fenêtre de terminal
FORCE_DESTROY=1 make destroy

Avant de tester un playbook destructif (changement de SELinux, partitionnement, durcissement OS), prenez un snapshot des 4 VMs :

Fenêtre de terminal
make snapshot # Crée un snapshot 'lab-checkpoint-<timestamp>' sur les 4 VMs
make restore # Restaure les 4 VMs depuis le dernier snapshot

C’est un filet de sécurité pédagogique — sur une vraie fleet, vous ne snapshotez pas avant chaque playbook.

EnvironnementAlternative
Mac M1/M2 (pas de KVM nested)UTM + virtio-fs, ou Multipass + Lima
Windows + WSL2KVM-on-WSL2 (KVM nested activé), ou Multipass natif
Pas d’accès rootVagrant + VirtualBox (config Vagrantfile équivalente)
VMs cloud (Hetzner, OVH, AWS)Provisionnement via Terraform (cf. cours Terraform du site)
Tout-en-un Dockerdocker-compose avec 4 conteneurs rastasheep/ubuntu-sshd (limité : pas de systemd PID 1)

Les commandes Ansible des guides suivants restent identiques — seule l’infrastructure d’hébergement change.

SymptômeCauseFix
error: Failed to start network lab-ansibleConflit de sous-réseau avec un autre réseau libvirtLister avec virsh net-list --all puis adapter la plage IP dans provision.sh
Permission denied sur /var/lib/libvirt/images/Utilisateur pas dans le groupe libvirtsudo usermod -aG libvirt $USER && newgrp libvirt
VM bloquée à [OK] Started cloud-init.serviceImage qcow2 corrompueSupprimer /var/lib/libvirt/images/AlmaLinux-10-*.qcow2, rejouer make provision
ssh: connect to host 10.10.20.10 port 22: Connection refusedVM démarrée mais SSH pas encore prêtAttendre 30 s supplémentaires, ou virsh console control-node pour suivre le boot
Permission denied (publickey) sur le premier pingMauvais utilisateur SSH ou clé mal propagéeVérifier ansible_user: ansible dans inventory/hosts.yml et ~/.ssh/authorized_keys côté VM
Connexion redirigée vers la mauvaise IP~/.ssh/config avec un bloc Host 10.* ProxyJumpLancer ssh -vvv ansible@10.10.20.21 pour voir l’IP réelle, surcharger via ~/.ssh/config.d/lab-ansible.conf
  • Le lab utilise virt-install + cloud-init pour provisionner 4 VMs AlmaLinux 10 reproductibles.
  • Les baux DHCP statiques côté libvirt garantissent des IPs stables sans toucher à NetworkManager — la parade qui marche sur RHEL 10.
  • Le cloud-init reste minimal : utilisateur, clé SSH, sudo NOPASSWD. Le reste est fait par Ansible.
  • Une seule commande make provision monte tout, make destroy démantèle proprement.
  • L’image cloud AlmaLinux est mise en cache entre runs pour économiser le téléchargement.
  • Si KVM n’est pas disponible, Vagrant, Multipass ou des VMs cloud prennent le relais sans changer les commandes Ansible.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn