
Un rôle validé sur une seule distribution n'est pas un rôle portable, c'est un rôle non testé ailleurs. Cette page étend la matrice Molecule à Rocky 9, AlmaLinux 10 et Debian 12, puis rend un rôle webserver capable de passer sur les trois. Les écarts ne sont pas théoriques : le nom du gestionnaire de paquets, l'emplacement de la racine web et l'utilisateur du service changent d'une famille à l'autre.
La technique tient en deux pièces, une liste de plateformes dans le scénario et un jeu de variables par famille dans le rôle. Ce qui demande de l'attention, ce sont les endroits où l'abstraction s'arrête.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Déclarer plusieurs plateformes et choisir des images capables de démarrer un service.
- Regrouper les instances par famille pour cibler vos vérifications.
- Écrire un rôle qui charge ses variables selon la distribution rencontrée.
- Repérer ce que le module
packagen'abstrait pas. - Vérifier un chemin qui diffère d'une distribution à l'autre, sans écrire un test complaisant.
Prérequis
Section intitulée « Prérequis »- Un scénario Molecule fonctionnel, monté selon Installer Molecule et sa CLI.
- Avoir suivi Cycle TDD complet pour l'écriture des assertions.
- Podman en mode utilisateur, avec de la place disque : trois images valent environ 1 Go.
La matrice de test
Section intitulée « La matrice de test »Chaque entrée de platforms: produit une instance, et les trois sont créées puis convergées en une seule passe. Le choix de l'image est le point délicat de cette section.
---driver: name: podman
platforms: - name: rocky9 image: docker.io/rockylinux/rockylinux:9-ubi-init pre_build_image: true privileged: true command: /sbin/init groups: - rhel_family
- name: alma10 image: docker.io/almalinux/10-init:10.2 pre_build_image: true privileged: true command: /sbin/init groups: - rhel_family
- name: debian12 # ce depot ne publie que `latest` : seul le digest fige l'image image: docker.io/geerlingguy/docker-debian12-ansible@sha256:e642ef9fc4932231fdd98b553fff0b1880df89d7bb64f9baff3c85811286cb5b pre_build_image: true privileged: true command: /lib/systemd/systemd groups: - debian_familyLe rôle démarre un service, donc chaque conteneur doit faire tourner un gestionnaire de services. C'est la raison de privileged: true et de la clé command:, qui remplace le processus initial du conteneur par systemd.
Cette exigence élimine les images de base, et le message d'erreur ne l'explique pas :
[ERROR]: Task failed: Module failed: Container rocky9 exited with code 127 when runedL'image rockylinux/rockylinux:9 ne contient pas systemd du tout, donc /sbin/init n'existe pas et le conteneur meurt à la seconde. Les variantes suffixées -init, ici rockylinux:9-ubi-init et almalinux/10-init, embarquent systemd et démarrent correctement.
$ podman run --rm docker.io/rockylinux/rockylinux:9 rpm -q systemdpackage systemd is not installedLe chemin de systemd diffère lui aussi selon la famille : /sbin/init du côté RHEL, /lib/systemd/systemd sur Debian. Une erreur de chemin donne exactement le même code 127.
Les groupes d'inventaire
Section intitulée « Les groupes d'inventaire »La clé groups: ne sert pas à la création du conteneur, le driver Podman l'ignore. Elle est lue par Molecule, qui bâtit l'inventaire remis à Ansible et y range chaque instance.
# inventaire généré par Moleculerhel_family: hosts: alma10: rocky9:debian_family: hosts: debian12:Vous pouvez donc écrire une vérification qui ne s'applique qu'à une famille, avec hosts: debian_family dans un playbook, sans jamais tester ansible_os_family à la main. L'inventaire indique par ailleurs que la connexion se fait en ansible_connection: podman : aucun serveur SSH n'est nécessaire dans les images.
Quel fact utiliser pour distinguer les distributions
Section intitulée « Quel fact utiliser pour distinguer les distributions »Avant d'écrire le moindre fichier de variables, il faut choisir sur quel critère les nommer. Ansible collecte plusieurs facts d'identification, et ils ne découpent pas la matrice de la même façon. Voici ce que les trois instances rapportent réellement.
| Instance | ansible_os_family | ansible_distribution | ansible_pkg_mgr |
|---|---|---|---|
| rocky9 | RedHat | Rocky | dnf |
| alma10 | RedHat | AlmaLinux | dnf |
| debian12 | Debian | Debian | apt |
ansible_os_family regroupe, et c'est précisément ce qu'on cherche ici : deux fichiers de variables suffisent pour trois distributions, parce que Rocky et AlmaLinux se comportent identiquement. ansible_distribution sépare, et imposerait un fichier par distribution, dont deux strictement identiques.
Le troisième fact explique le fonctionnement du module package : Ansible connaît déjà le gestionnaire de chaque machine, et n'a pas besoin que vous le lui disiez. Un quatrième, ansible_service_mgr, vaut systemd sur les trois, ce qui confirme que les images choisies font bien tourner un gestionnaire de services.
Nommez donc vos fichiers d'après ansible_os_family par défaut, et ne descendez au niveau de la distribution que si un écart réel l'impose, par exemple un nom de paquet propre à une seule d'entre elles.
Le rôle portable
Section intitulée « Le rôle portable »Les variables qui changent
Section intitulée « Les variables qui changent »Le principe est de sortir des tâches tout ce qui dépend de la distribution, et de le ranger dans des fichiers nommés d'après la famille d'OS. Les deux fichiers portent exactement les mêmes clés, avec des valeurs différentes.
---__webserver_package_name: nginx__webserver_service_name: nginx__webserver_html_dir: /usr/share/nginx/html__webserver_user: nginx---__webserver_package_name: nginx__webserver_service_name: nginx__webserver_html_dir: /var/www/html__webserver_user: www-dataLe double tiret bas en préfixe est une convention qui signale des variables internes au rôle. Elle ne verrouille rien techniquement, mais elle distingue au premier regard ce que l'utilisateur du rôle peut redéfinir de ce qui relève de la plomberie interne.
L'écart mérite d'être constaté plutôt que cru sur parole. Après convergence, la configuration de nginx ne désigne pas le même utilisateur selon la machine :
rocky9 user nginx;alma10 user nginx;debian12 user www-data;Charger les variables et poser le service
Section intitulée « Charger les variables et poser le service »Le chargement se fait en première tâche, car les suivantes en dépendent. Le fact ansible_os_family vaut RedHat pour Rocky comme pour AlmaLinux, et Debian pour Debian, ce qui fait tomber les trois plateformes sur les deux seuls fichiers écrits.
---- name: Charger les variables spécifiques à la distribution ansible.builtin.include_vars: "{{ ansible_os_family }}.yml"
- name: Rafraîchir le cache de paquets APT ansible.builtin.apt: update_cache: true cache_valid_time: 3600 when: ansible_os_family == 'Debian'
- name: Installer le serveur web ansible.builtin.package: name: "{{ __webserver_package_name }}" state: present
- name: Déployer la page d'accueil ansible.builtin.copy: dest: "{{ __webserver_html_dir }}/index.html" content: "Hello\n" mode: "0644"
- name: Démarrer le service ansible.builtin.systemd_service: name: "{{ __webserver_service_name }}" state: started enabled: trueLe module ansible.builtin.package choisit tout seul dnf ou apt selon la machine. Il rend ce rôle indifférent au gestionnaire de paquets, à condition que le paquet porte le même nom partout, ce qui est le cas de nginx mais pas de tous.
Ce que package ne fait pas à votre place
Section intitulée « Ce que package ne fait pas à votre place »La tâche apt conditionnelle du bloc précédent n'est pas une précaution superflue. Sans elle, la convergence réussit sur les deux distributions RHEL et échoue sur la troisième :
fatal: [debian12]: FAILED! => {"msg": "No package matching 'nginx' is available"}alma10 : ok=5 changed=3 failed=0debian12 : ok=2 changed=0 failed=1rocky9 : ok=5 changed=3 failed=0Le paquet existe pourtant dans les dépôts Debian. Ce qui manque est l'index local des paquets, vide dans une image fraîchement téléchargée. dnf rafraîchit ses métadonnées de lui-même, apt ne le fait jamais sans qu'on le demande. L'abstraction de package porte donc sur l'installation, pas sur la préparation du gestionnaire.
Le cache_valid_time: 3600 évite de retélécharger l'index à chaque exécution, ce qui garde l'idempotence et accélère les convergences répétées pendant le développement.
C'est le genre d'écart que la matrice révèle immédiatement. Sur un poste RHEL, ce rôle aurait été jugé correct et livré tel quel.
Vérifier sur les trois distributions
Section intitulée « Vérifier sur les trois distributions »Le playbook de vérification a besoin des mêmes variables que le rôle, puisque le chemin à contrôler diffère selon la machine. Il les recharge depuis le répertoire du rôle, avec un chemin relatif au répertoire du scénario.
---- name: Verify hosts: all gather_facts: true tasks: - name: Charger les vars distro pour la vérification ansible.builtin.include_vars: "../../vars/{{ ansible_os_family }}.yml"
- name: Relever les paquets installés ansible.builtin.package_facts:
- name: Assertion le serveur web est installé ansible.builtin.assert: that: - "'nginx' in ansible_facts.packages" fail_msg: "nginx absent sur {{ ansible_distribution }}"
- name: Lire la page à l'emplacement propre à la distribution ansible.builtin.slurp: src: "{{ __webserver_html_dir }}/index.html" register: index
- name: Assertion contenu de la page ansible.builtin.assert: that: - "'Hello' in (index.content | b64decode)"Le ../../ compte deux niveaux depuis molecule/default/ et retombe sur la racine du rôle, où se trouve vars/. Un chemin qui repasserait par roles/webserver/ cherche un répertoire inexistant et fait échouer les trois instances d'un coup.
Contrôler le bon chemin n'est pas un détail de forme. Sur Debian, /usr/share/nginx/html/index.html existe et contient la page d'accueil par défaut de nginx : une assertion qui viserait ce chemin partout trouverait un fichier sur les trois machines et passerait au vert sans jamais constater que le rôle a déposé sa page au bon endroit.
Le résultat attendu couvre les trois plateformes, avec une idempotence vérifiée sur chacune.
INFO [default > converge] Executed: Successfulalma10 : ok=5 changed=0debian12 : ok=6 changed=0 (idempotence)rocky9 : ok=5 changed=0alma10 : ok=6 changed=0debian12 : ok=6 changed=0 (verify)rocky9 : ok=6 changed=0Debian compte une tâche de plus, celle du cache APT, que les deux autres sautent grâce au when:.
Cibler une seule distribution
Section intitulée « Cibler une seule distribution »Quand une seule plateforme échoue, relancer les trois coûte plusieurs minutes pour rien. L'option --platform-name restreint le cycle à l'instance nommée.
molecule test --platform-name rocky9Cette option a une limite qui surprend : elle n'existe que sur molecule test. Les commandes create, converge, verify et destroy la refusent avec Error: No such option: -p. Pour déboguer pas à pas sur une seule distribution, la voie praticable consiste donc à réduire temporairement la liste platforms: du scénario.
Ce que cette matrice ne teste pas
Section intitulée « Ce que cette matrice ne teste pas »Une matrice au vert donne une confiance réelle, à condition de savoir où elle s'arrête. Trois limites tiennent à la nature même du conteneur, et aucune n'apparaît dans la sortie de Molecule.
La première est le noyau. Les trois instances partagent celui de la machine hôte, quelle que soit la distribution qu'elles prétendent être.
noyau hôte : 6.8.0-134-genericnoyau rocky9 : 6.8.0-134-genericnoyau debian12 : 6.8.0-134-genericUn rôle qui charge un module noyau, ajuste un paramètre sysctl ou dépend d'une version de noyau précise n'est donc pas validé par ce test, même au vert sur les trois plateformes.
La deuxième tient à l'environnement d'exécution. Le mode privilégié et le remplacement du processus initial recréent un système plausible, pas un système installé : pas de partitionnement, pas de démarrage réel, pas de pare-feu ni de politique SELinux active dans le cas général. Un rôle qui touche à ces couches demande une machine virtuelle.
La troisième est la connexion. Molecule se branche ici en podman, sans passer par SSH. Tout ce qui relève de la configuration du service SSH, des clés ou du mode de connexion échappe à la vérification.
Cela ne disqualifie pas la matrice, qui reste le moyen le plus rapide d'attraper les écarts de paquets, de chemins et d'utilisateurs, c'est-à-dire l'écrasante majorité des problèmes de portabilité. Cela indique où placer la limite entre ce qui se teste en conteneur et ce qui exige un environnement complet.
Mettre en pratique
Section intitulée « Mettre en pratique »Un rôle qui passe sur Rocky Linux ne prouve rien sur Debian, et c'est précisément ce que cette matrice sert à révéler. Ce lab vous fait étendre le scénario à trois distributions, charger les valeurs propres à chacune depuis vars/<os_family>.yml, puis absorber les écarts de noms de paquets avec le module package. La correction joue le verify.yml sur les trois instances : un chemin écrit en dur dans le rôle en fait tomber au moins une.
Pièges courants
Section intitulée « Pièges courants »| Symptôme | Cause | Solution |
|---|---|---|
Container ... exited with code 127 | L'image ne contient pas systemd, ou le command: pointe ailleurs | Prendre une image -init, adapter /sbin/init ou /lib/systemd/systemd |
No package matching 'nginx' sur Debian seulement | Index APT vide, package ne le rafraîchit pas | Tâche apt: update_cache: true conditionnée à la famille Debian |
| Les trois instances échouent au verify | Chemin d'include_vars erroné depuis le scénario | Compter les niveaux depuis molecule/default/, soit ../../vars/ |
| Une variable manque sur une seule distribution | Les fichiers vars/ n'ont pas les mêmes clés | Garder des jeux de clés identiques, seules les valeurs changent |
| Verify vert alors que la page est au mauvais endroit | Le chemin testé existe sur les deux familles | Vérifier __webserver_html_dir, jamais un chemin écrit en dur |
Error: No such option: -p | --platform-name n'existe que sur molecule test | Réduire temporairement la liste platforms: |
À retenir
Section intitulée « À retenir »- Une plateforme par distribution suffit à transformer un rôle supposé portable en rôle vérifié.
- Les images de base n'ont pas systemd : les variantes
-initsont nécessaires dès que le rôle démarre un service. groups:est lu par Molecule, pas par le driver, et permet de viser une famille depuis un playbook.packageabstrait l'installation, pas le rafraîchissement du cache, qui reste propre à APT.- Les fichiers
vars/<famille>.ymlportent les mêmes clés avec des valeurs différentes, sinon l'échec ne survient que sur une distribution. --platform-namen'existe que surmolecule testet ne dépanne pas les commandes unitaires.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Tests tox multi-versions : le second axe de la matrice, les versions d'ansible-core.
- CI GitHub Actions : croiser distributions et versions dans un workflow, actions épinglées par SHA.
- CI GitLab : le même croisement en
parallel:matrix, si votre forge est GitLab.