📍 Article principal : Ansible 2026 : la stack pratique pour automatiser Linux et Windows
Vos rôles sont propres et durcis. On les teste maintenant — automatiquement, dans des conteneurs éphémères, à chaque pull request.
Ce que vous aurez à la fin
Une suite Molecule pour le rôle nginx (ou n’importe quel autre rôle) qui provisionne un conteneur Docker, applique le rôle, vérifie l’idempotence, lance un jeu d’assertions, puis détruit l’environnement. Le tout s’exécute en local en quelques minutes et tourne automatiquement dans GitHub Actions à chaque push. Vous pourrez ajouter de nouvelles assertions sans toucher à l’infrastructure de tests, et matricer le rôle sur Debian, Ubuntu et Rocky Linux dans le même run.
Prérequis
- ansible-core 2.20 et un rôle existant à tester (par exemple celui produit dans le tutoriel sur la structuration en rôles).
- Docker Engine installé localement (Docker Desktop sur macOS/Windows, paquet
docker-cesur Linux). Podman fonctionne aussi avec une configuration équivalente. - Python 3.10 minimum sur le poste, et pipx déjà disponible.
- Niveau attendu : avoir écrit un rôle Ansible avec au moins trois tâches.
- Temps : 90 minutes en lecture active.
Étape 1 — Pourquoi Molecule plutôt que des scripts maison
Tester un rôle Ansible signifie répondre à trois questions. Le rôle s’applique-t-il sans erreur sur l’OS cible ? Est-il idempotent (un second run ne fait rien) ? Le résultat correspond-il à ce qu’on attendait (services démarrés, fichiers en place, ports ouverts) ?
On peut bricoler une réponse avec des scripts shell : docker run, ansible-playbook, docker exec pour vérifier, docker rm. Au début ça marche. Au bout de cinq rôles, le ménage des conteneurs orphelins, la gestion des matrices d’OS et l’intégration CI deviennent un projet à part entière.
Molecule encapsule tout cela dans un outil dédié. Le cycle est figé (create → converge → idempotence → verify → destroy), les drivers (Docker, Podman, EC2, OpenStack) sont interchangeables, l’intégration CI est triviale, et l’écosystème publie depuis longtemps des images de cibles bien préparées.
Étape 2 — Installer Molecule et ses dépendances
Molecule est distribué comme un paquet Python. La méthode propre consiste à l’installer via pipx, dans un environnement isolé qui partage avec ansible-core. On installe également molecule-plugins qui contient les drivers (Docker, EC2, etc.) :
pipx install molecule
pipx inject molecule molecule-plugins[docker] ansible-lint pytest-testinfra
La commande pipx inject ajoute des paquets Python à l’environnement de molecule sans casser son isolation. ansible-lint sera utilisé par Molecule pour valider les rôles, et pytest-testinfra sert à écrire les assertions de vérification — c’est le framework standard pour interroger un système Linux depuis Python.
Vérification immédiate :
molecule --version
La sortie doit afficher Molecule en version 26.x ou supérieure (la 26.4.0 d’avril 2026 est la branche active). Si la version est plus ancienne, on force la mise à jour avec pipx upgrade molecule.
Une note importante avant de continuer. Molecule 26 introduit un modèle de configuration ansible-native qui remplace progressivement le modèle historique à drivers (docker, podman, ec2). Le modèle legacy, désormais appelé pre ansible-native, reste maintenu pour les rôles existants et la majorité des projets en 2026 — c’est ce que nous utilisons dans ce tutoriel. Une fois à l’aise avec ce modèle, la transition vers l’ansible-native consiste à remplacer le bloc driver par des playbooks create.yml/destroy.yml qui invoquent directement les modules de la collection containers.podman ou community.docker.
Étape 3 — Initialiser un scénario Molecule
On se place à la racine d’un rôle existant et on initialise un scénario par défaut :
cd roles/nginx
molecule init scenario --driver-name docker default
Cette commande crée le répertoire molecule/default/ avec quatre fichiers :
roles/nginx/molecule/default/
├── molecule.yml # configuration du scénario : drivers, plateformes, étapes
├── converge.yml # le playbook qui applique le rôle dans le conteneur
├── verify.yml # le playbook ou les tests qui vérifient le résultat
└── create.yml # rare à modifier — création du conteneur (livré par défaut)
Cette structure est ce qui rend Molecule prévisible. Tous les rôles Molecule de la planète ont la même organisation, et un développeur qui arrive comprend l’arborescence en trente secondes.
Étape 4 — Configurer molecule.yml
Le fichier molecule/default/molecule.yml est le cœur de la configuration. On le réécrit pour matricer le rôle sur trois OS :
---
dependency:
name: galaxy
options:
requirements-file: requirements.yml
driver:
name: docker
platforms:
- name: nginx-debian-12
image: geerlingguy/docker-debian12-ansible:latest
pre_build_image: true
privileged: true
cgroupns_mode: host
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
command: /lib/systemd/systemd
- name: nginx-ubuntu-2404
image: geerlingguy/docker-ubuntu2404-ansible:latest
pre_build_image: true
privileged: true
cgroupns_mode: host
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
command: /lib/systemd/systemd
- name: nginx-rocky-9
image: geerlingguy/docker-rockylinux9-ansible:latest
pre_build_image: true
privileged: true
cgroupns_mode: host
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
command: /usr/lib/systemd/systemd
provisioner:
name: ansible
config_options:
defaults:
callback_whitelist: timer,profile_tasks
inventory:
host_vars:
nginx-debian-12:
ansible_python_interpreter: /usr/bin/python3
nginx-ubuntu-2404:
ansible_python_interpreter: /usr/bin/python3
nginx-rocky-9:
ansible_python_interpreter: /usr/bin/python3
verifier:
name: ansible
Plusieurs détails comptent. Les images geerlingguy/docker-*-ansible sont préinstallées avec systemd opérationnel — ce qui permet aux rôles d’utiliser le module ansible.builtin.systemd_service sans bidouille. Les options privileged, cgroupns_mode: host et le volume /sys/fs/cgroup sont les directives qui rendent systemd fonctionnel dans un conteneur en 2026 — sans elles, systemctl tombe en erreur.
Le command: /lib/systemd/systemd remplace l’entrypoint par défaut. Sur Rocky Linux et autres dérivés RHEL, le chemin est /usr/lib/systemd/systemd ; on adapte plate-forme par plate-forme.
Les callbacks timer et profile_tasks activés sur le provisioner ajoutent à la sortie le temps total et le détail par tâche — précieux pour repérer les goulots quand le test devient lent.
Étape 5 — Le playbook converge
C’est ici qu’on applique le rôle dans les conteneurs. Le fichier molecule/default/converge.yml :
---
- name: Converge
hosts: all
become: true
pre_tasks:
- name: Mettre à jour le cache APT (Debian/Ubuntu)
ansible.builtin.apt:
update_cache: true
cache_valid_time: 600
when: ansible_os_family == 'Debian'
changed_when: false
- name: Mettre à jour le cache DNF (Rocky)
ansible.builtin.dnf:
update_cache: true
when: ansible_os_family == 'RedHat'
changed_when: false
roles:
- role: nginx
vars:
nginx_default_site:
server_name: test.local
root: /var/www/html
index: index.html
On expose explicitement les variables qu’un utilisateur du rôle pourrait surcharger — c’est aussi un moyen de documenter par l’exemple. Les pre_tasks mettent à jour le cache du gestionnaire de paquets selon la famille d’OS. Le drapeau changed_when: false évite que ces tâches comptent comme un changement et faussent le test d’idempotence.
Étape 6 — Le playbook verify
L’étape verify est ce qui transforme un déploiement en test. Elle s’exécute après converge et après le test d’idempotence ; on y vérifie que le système est dans l’état attendu. molecule/default/verify.yml :
---
- name: Verify
hosts: all
become: true
tasks:
- name: Vérifier que nginx est installé
ansible.builtin.package_facts:
manager: auto
- name: Assertion — paquet nginx présent
ansible.builtin.assert:
that: "'nginx' in ansible_facts.packages"
fail_msg: "Le paquet nginx n'est pas installé"
- name: Vérifier que le service nginx tourne
ansible.builtin.service_facts:
- name: Assertion — service nginx actif
ansible.builtin.assert:
that:
- "'nginx.service' in ansible_facts.services"
- "ansible_facts.services['nginx.service'].state == 'running'"
- name: Tester l'écoute sur le port 80
ansible.builtin.wait_for:
port: 80
host: 127.0.0.1
timeout: 5
- name: Récupérer la page d'accueil
ansible.builtin.uri:
url: http://127.0.0.1/
status_code: [200, 403]
return_content: true
register: home_response
- name: Assertion — réponse contient bien du HTML
ansible.builtin.assert:
that: home_response.content is match('.*<[hH][tT][mM][lL].*')
fail_msg: "La page servie par nginx ne contient pas de HTML"
Le module ansible.builtin.assert est l’outil de prédilection pour les vérifications. Il échoue le play si la condition est fausse, avec un message clair. Combiné à package_facts et service_facts, on couvre la majorité des assertions courantes sans dépendance externe.
Étape 7 — Lancer le cycle complet
On lance le cycle Molecule complet :
cd roles/nginx
molecule test
La commande exécute la séquence : dependency (installe collections déclarées), lint (passe ansible-lint), cleanup (purge les conteneurs résiduels), destroy (au cas où), syntax (valide le playbook), create (lance les conteneurs), prepare (étape optionnelle), converge (applique le rôle), idempotence (relance le converge et exige changed=0), side_effect (optionnel), verify (lance les assertions), cleanup, destroy.
Sur la matrice à trois OS, comptez sept à dix minutes la première fois — les images Docker sont téléchargées (350 Mo chacune environ). Les exécutions suivantes prennent deux à trois minutes grâce au cache Docker.
Pendant le développement, la séquence test est trop lente pour être pratique à chaque modification. On utilise alors les sous-commandes :
molecule create # lance les conteneurs et s'arrête
molecule converge # applique le rôle (rapide)
molecule verify # lance les assertions (rapide)
molecule destroy # nettoie
Le pattern itératif consiste à create une fois, puis converge + verify à chaque modification, et destroy à la fin de la session.
Étape 8 — Plusieurs scénarios pour plusieurs cas d’usage
Un rôle complexe peut avoir plusieurs configurations à tester. Le rôle nginx par exemple doit fonctionner avec et sans HTTPS, avec et sans modules supplémentaires, avec différents nombres de vhosts. On crée des scénarios complémentaires :
molecule init scenario --driver-name docker https
molecule init scenario --driver-name docker multi-vhost
Chaque scénario vit sous molecule/<nom>/ avec sa propre molecule.yml, son propre converge.yml, son propre verify.yml. On les lance individuellement :
molecule test --scenario-name https
molecule test --scenario-name multi-vhost
Pour exécuter tous les scénarios en cascade : molecule test --all. La séparation par scénarios évite les configurations switch-case imbriquées dans un unique molecule.yml.
Étape 9 — Intégration GitHub Actions
Le bénéfice complet de Molecule arrive quand chaque pull request lance la suite automatiquement. Le workflow .github/workflows/molecule.yml :
name: Molecule
on:
push:
branches: [main]
pull_request:
jobs:
molecule:
runs-on: ubuntu-latest
strategy:
matrix:
scenario: [default]
platform:
- debian-12
- ubuntu-2404
- rocky-9
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Installer Molecule et ansible-core
run: |
pip install ansible-core==2.20.* molecule molecule-plugins[docker] \
ansible-lint pytest-testinfra
- name: Installer les collections du rôle
run: ansible-galaxy collection install -r requirements.yml
- name: Lancer Molecule
env:
PY_COLORS: '1'
ANSIBLE_FORCE_COLOR: '1'
run: molecule test --scenario-name ${{ matrix.scenario }}
La matrice GitHub Actions multiplie les jobs : un par OS, en parallèle. Une PR qui casse Rocky Linux mais pas Debian apparaît immédiatement avec un échec ciblé. Le job dure cinq à huit minutes selon la taille du rôle ; sur les runners GitHub gratuits, ça reste largement dans les budgets.
Étape 10 — Tests d’infrastructure avec testinfra (option avancée)
Le verify.yml en YAML est suffisant pour 90 % des cas. Pour des assertions plus riches — manipulation de fichiers binaires, tests réseaux complexes, validations métier — Molecule supporte aussi testinfra, un framework Python qui permet d’écrire des tests pytest interrogeant l’infrastructure.
On bascule le verifier dans molecule.yml :
verifier:
name: testinfra
options:
verbose: true
Et on écrit les tests dans molecule/default/tests/test_default.py :
import pytest
@pytest.mark.parametrize('pkg', ['nginx'])
def test_packages_installed(host, pkg):
assert host.package(pkg).is_installed
def test_nginx_running(host):
nginx = host.service('nginx')
assert nginx.is_running
assert nginx.is_enabled
def test_nginx_listens(host):
assert host.socket('tcp://0.0.0.0:80').is_listening
def test_index_html_served(host):
cmd = host.run('curl -s http://127.0.0.1/')
assert cmd.rc == 0
assert '<html' in cmd.stdout.lower()
Testinfra apporte la lisibilité de pytest et la composition par fixtures. Sur les rôles partagés inter-équipes, le retour sur investissement se voit rapidement — chaque équipe ajoute ses propres assertions sans toucher au reste.
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
systemctl échoue dans le conteneur |
Image sans systemd ou sans privileged |
Utiliser une image geerlingguy/docker-*-ansible et activer privileged: true |
| Test d’idempotence rouge | Tâche shell ou command sans garde |
Ajouter changed_when: conditionnel, ou utiliser un module dédié |
Conteneurs orphelins après Ctrl+C |
destroy non appelé |
molecule destroy ou docker rm -f $(docker ps -aq -f label=molecule) |
Failed to find required executable apt-get |
Image sans gestionnaire de paquet attendu | Adapter les pre_tasks à la famille OS via when: ansible_os_family |
| CI tombe sur Docker daemon not running | Job runner sans Docker activé | Sur GitHub Actions, ubuntu-latest a Docker préinstallé ; sur self-hosted, vérifier systemctl status docker |
| Tests Molecule très lents | Téléchargement des images à chaque run | Activer le cache Docker côté CI, ou utiliser actions/cache sur ~/.cache/molecule |
Tutoriels frères
Pour étoffer le tableau
- Documentation officielle Molecule
- Images Docker Ansible de Jeff Geerling
- testinfra — assertions d’infrastructure en pytest
- 🔝 Retour à l’article principal du dossier Ansible
FAQ
Docker ou Podman pour Molecule ?
Les deux fonctionnent. Docker reste plus simple à mettre en place sur GitHub Actions et offre la documentation la plus riche. Podman a l’avantage de tourner sans démon root, plus aligné avec les pratiques de sécurité modernes ; on l’utilise via driver: podman dans molecule.yml. Pour un rôle qui utilise systemd, vérifier que la version de Podman supporte les conteneurs avec --systemd=true.
Faut-il un scénario par OS ou un scénario qui matrice les OS ?
Un seul scénario qui matrice les OS dans platforms est plus simple à maintenir et garantit des assertions identiques sur toutes les cibles. On crée un scénario distinct uniquement quand la configuration testée diffère vraiment (HTTP vs HTTPS, mode standalone vs cluster), pas quand seul l’OS change.
Que faire d’un test d’idempotence qui échoue après un upgrade APT légitime ?
On exclut la tâche du compte d’idempotence en lui ajoutant changed_when: false si elle peut légitimement faire un changement à chaque run (mise à jour de cache APT par exemple). Pour les tâches qui doivent réellement changer la première fois et pas la suivante, on cherche le module idempotent natif au lieu de shell.
Combien de temps un test Molecule prend-il sur un rôle moyen ?
Trois à cinq minutes pour un seul OS, sept à dix minutes pour trois OS en parallèle local. En CI sur runner partagé, comptez le double — c’est encore acceptable pour un déclenchement par PR.
Molecule peut-il tester un rôle qui modifie des modules kernel ?
Pas dans un conteneur. Les modules kernel ne se chargent que sur la machine hôte ou dans une vraie VM. On utilise alors le driver delegated qui pointe sur une instance EC2 ou un VPS éphémère, ou on bascule sur vagrant en local pour des VM complètes. Les performances en pâtissent, mais c’est le seul moyen.
Doit-on versionner les fichiers Molecule dans le rôle ?
Oui, absolument. Le scénario par défaut fait partie de la documentation exécutable du rôle. Un développeur qui clone le rôle doit pouvoir lancer molecule test immédiatement. C’est aussi ce qui permet à Galaxy d’afficher un badge « tested with Molecule ».