Aller au contenu
Infrastructure as Code medium

Scénarios Molecule multi-distribution : Rocky, AlmaLinux, Debian

90 min de lecture

Logo Ansible

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.

  • 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 package n'abstrait pas.
  • Vérifier un chemin qui diffère d'une distribution à l'autre, sans écrire un test complaisant.

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.

molecule/default/molecule.yml
---
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_family

Le 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 runed

L'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.

Fenêtre de terminal
$ podman run --rm docker.io/rockylinux/rockylinux:9 rpm -q systemd
package systemd is not installed

Le 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.

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 Molecule
rhel_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.

Instanceansible_os_familyansible_distributionansible_pkg_mgr
rocky9RedHatRockydnf
alma10RedHatAlmaLinuxdnf
debian12DebianDebianapt

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 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.

vars/RedHat.yml
---
__webserver_package_name: nginx
__webserver_service_name: nginx
__webserver_html_dir: /usr/share/nginx/html
__webserver_user: nginx
vars/Debian.yml
---
__webserver_package_name: nginx
__webserver_service_name: nginx
__webserver_html_dir: /var/www/html
__webserver_user: www-data

Le 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;

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.

tasks/main.yml
---
- 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: true

Le 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.

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=0
debian12 : ok=2 changed=0 failed=1
rocky9 : ok=5 changed=3 failed=0

Le 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.

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.

molecule/default/verify.yml
---
- 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: Successful
alma10 : ok=5 changed=0
debian12 : ok=6 changed=0 (idempotence)
rocky9 : ok=5 changed=0
alma10 : ok=6 changed=0
debian12 : ok=6 changed=0 (verify)
rocky9 : ok=6 changed=0

Debian compte une tâche de plus, celle du cache APT, que les deux autres sautent grâce au when:.

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.

Fenêtre de terminal
molecule test --platform-name rocky9

Cette 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.

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-generic
noyau rocky9 : 6.8.0-134-generic
noyau debian12 : 6.8.0-134-generic

Un 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.

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.

SymptômeCauseSolution
Container ... exited with code 127L'image ne contient pas systemd, ou le command: pointe ailleursPrendre une image -init, adapter /sbin/init ou /lib/systemd/systemd
No package matching 'nginx' sur Debian seulementIndex APT vide, package ne le rafraîchit pasTâche apt: update_cache: true conditionnée à la famille Debian
Les trois instances échouent au verifyChemin d'include_vars erroné depuis le scénarioCompter les niveaux depuis molecule/default/, soit ../../vars/
Une variable manque sur une seule distributionLes fichiers vars/ n'ont pas les mêmes clésGarder des jeux de clés identiques, seules les valeurs changent
Verify vert alors que la page est au mauvais endroitLe chemin testé existe sur les deux famillesVérifier __webserver_html_dir, jamais un chemin écrit en dur
Error: No such option: -p--platform-name n'existe que sur molecule testRéduire temporairement la liste platforms:
  • 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 -init sont 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.
  • package abstrait l'installation, pas le rafraîchissement du cache, qui reste propre à APT.
  • Les fichiers vars/<famille>.yml portent les mêmes clés avec des valeurs différentes, sinon l'échec ne survient que sur une distribution.
  • --platform-name n'existe que sur molecule test et ne dépanne pas les commandes unitaires.

Ce site vous est utile ?

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

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn