Ansible Molecule

Siehe auch

Verwandte Artikel
Offizielle Dokumentation
Linuxfabrik

Molecule ist das Standard-Test-Framework für Ansible-Rollen und -Collections. Es provisioniert eine isolierte Testumgebung (typischerweise einen Container), spielt dort die zu testende Rolle ein, prüft das Ergebnis und räumt hinterher wieder auf. So lassen sich Ansible-Automatisierungen reproduzierbar testen, ohne eine echte Zielmaschine zu beanspruchen. Molecule gehört zu den ansible-dev-tools und wird von Red Hat als offizielles Testwerkzeug gepflegt.

Begriffe
  • converge: die eigentliche Test-Phase - hier läuft das zu testende Playbook gegen die Testumgebung.

  • Driver: das Backend, das die Testumgebung provisioniert. Gebräuchlich sind podman, docker, vagrant und default (für bereits vorhandene oder selbst provisionierte Umgebungen).

  • idempotence: zweiter Lauf von converge. Ansible muss dabei changed=0 melden, sonst ist die Rolle nicht idempotent.

  • Szenario: ein Satz Testdateien unter extensions/molecule/<name>/. Jede Collection bringt mindestens ein Szenario (typischerweise default) mit. Weitere Szenarien für spezifische OS-Varianten (z.B. rhel, debian) sind üblich.

  • verify: optionale Akzeptanzprüfung mit einem externen Tool, z.B. Testinfra oder Ansible-Assertions.

Installation

Die Installation erfolgt in einer dedizierten venv. Als Driver-Backend dient podman, rootless und auf RHEL 8+ standardmässig verfügbar.

dnf --assumeyes install podman

python3 -m venv ~/venvs/molecule
source ~/venvs/molecule/bin/activate

python3 -m pip install --upgrade pip
python3 -m pip install ansible-dev-tools 'molecule-plugins[podman]'

ansible-galaxy collection install containers.podman community.general

Versionen prüfen:

molecule --version
ansible --version

Szenario in einer Collection anlegen

Innerhalb einer bestehenden Ansible-Collection (z.B. lfops) wird ein Default-Szenario initialisiert:

source ~/venvs/molecule/bin/activate

cd lfops
mkdir --parents extensions
cd extensions

molecule init scenario
# optional: additional scenarios for specific OS variants
molecule init scenario rhel

Struktur nach init:

lfops/extensions/molecule/
+-- default/
|   +-- converge.yml
|   +-- create.yml
|   +-- destroy.yml
|   +-- molecule.yml
|   +-- verify.yml
+-- rhel/
    +-- ...

molecule init scenario legt ein generisches Gerüst an. create.yml und destroy.yml sind dabei nur Platzhalter und provisionieren noch keinen Container (siehe Podman als Driver). Einen Driver wählt init nicht mehr aus, das geschieht in der molecule.yml.

Verschachtelte und gruppierte Szenarien

Molecule erlaubt es, Szenarien verschachtelt und gruppiert anzulegen:

lfops/extensions/molecule/
+-- config.yml
+-- default/
|   +-- converge.yml
|   +-- create.yml
|   +-- destroy.yml
|   +-- molecule.yml
|   +-- verify.yml
+-- rhel/
|   +-- rhel8/
|   |   +-- converge.yml
|   |   +-- create.yml
|   |   +-- destroy.yml
|   |   +-- molecule.yml
|   |   +-- verify.yml
|   +-- rhel9/
|   |   +-- ...
|   +-- rhel10/
|       +-- ...
+-- debian/
    +-- debian11/
    |   +-- ...
    +-- debian12/
    |   +-- ...
    +-- debian13/
        +-- ...

Die einzelnen Szenarien werden mit ihrem relativen Pfad bezüglich extensions/molecule aufgerufen. Bei diesem Beispiellayout läuft das RHEL-9-Szenario mit molecule test --scenario-name rhel/rhel9.

Alle Szenarien einer Gruppe lassen sich über eine pfad-ähnliche Wildcard gemeinsam ausführen. Beim obigen Beispiel testet molecule test --scenario-name 'debian/*' die gesamte debian-Gruppe auf einmal.

Die config.yml direkt unter extensions/molecule/ ist eine geteilte Basiskonfiguration. Molecule merged sie in jedes Szenario; die molecule.yml eines Szenarios überschreibt davon nur einzelne Werte. So landet alles Gemeinsame (Dependency, Ansible-Optionen, test_sequence) zentral in der config.yml, und die Szenario-Dateien bleiben schlank. LFOps fährt diesen Weg: dort steht fast die gesamte Konfiguration in der config.yml, und die molecule.yml der Szenarien ist nahezu leer.

Bemerkung

Die Verschachtelung selbst ist eine reine Gruppierung. Zwischen den Szenarien einer Gruppe (z.B. rhel/rhel8 und rhel/rhel9) gibt es keine Vererbung von Konfiguration oder Playbooks. Geteilte Werte laufen ausschliesslich über die config.yml.

Konfiguration

Zentrale Datei ist molecule.yml pro Szenario. molecule init scenario erzeugt sie im aktuellen Format mit den drei Blöcken dependency, ansible und scenario:

extensions/molecule/default/molecule.yml
dependency:
  name: 'galaxy'

ansible:
  cfg:
    defaults:
      host_key_checking: false
  executor:
    backend: 'ansible-playbook'

scenario:
  name: 'default'
  test_sequence:
    - 'dependency'
    - 'cleanup'
    - 'destroy'
    - 'syntax'
    - 'create'
    - 'prepare'
    - 'converge'
    - 'idempotence'
    - 'side_effect'
    - 'verify'
    - 'cleanup'
    - 'destroy'
  • dependency installiert die in requirements.yml gelisteten Collections und Rollen per ansible-galaxy.

  • ansible konfiguriert den Ansible-Lauf. Unter cfg schreibt Molecule eine temporäre ansible.cfg, executor.backend wählt zwischen ansible-playbook und ansible-navigator. Dieser Block ersetzt die früheren Top-Level-Schlüssel provisioner und verifier.

  • scenario.test_sequence legt fest, welche Phasen molecule test durchläuft (siehe Testablauf).

Eine knappere Schreibweise mit den älteren Top-Level-Schlüsseln provisioner und verifier akzeptiert Molecule weiterhin:

extensions/molecule/default/molecule.yml
provisioner:
  name: 'ansible'

verifier:
  name: 'ansible'

Der zu testende Code landet in converge.yml:

extensions/molecule/default/converge.yml
- name: 'Converge'
  hosts: 'all'
  tasks:
    - name: 'apply my role'
      ansible.builtin.include_role:
        name: 'my_role'

Podman als Driver

Für den Podman-Driver kommen driver und platforms in die molecule.yml:

extensions/molecule/default/molecule.yml
driver:
  name: 'podman'

platforms:
  - name: 'rocky9'
    image: 'rockylinux/rockylinux:9-ubi-init'
    pre_build_image: true
    privileged: true
    command: '/usr/sbin/init'

Zwei Punkte sind entscheidend, damit der Lauf nicht abbricht:

  • Das Image muss ein Init-System (systemd) mitbringen, sobald die Rolle Dienste verwaltet. Das schlanke rockylinux/rockylinux:9 hat kein /usr/sbin/init und beendet sich sofort. Die -ubi-init-Variante (analog redhat/ubi9-init) startet systemd. command: '/usr/sbin/init' macht systemd zu PID 1, privileged: true ist dafür im Container nötig.

  • Die von init erzeugten Platzhalter create.yml und destroy.yml aus dem Szenario löschen. Ohne eigene create.yml/destroy.yml greift Molecule auf die mitgelieferten Playbooks des Podman-Drivers zurück, die den Container bauen und wieder abräumen.

Testablauf

Molecule unterteilt einen Testlauf in einzelne Phasen. Die per molecule init scenario erzeugte Default-Sequenz lautet:

  • dependency: Installation aller benötigten Abhängigkeiten auf dem Ansible Controller (Collections, Rollen, Playbooks).

  • cleanup: Entfernen externer Ressourcen, die ein vorheriger Lauf hinterlassen hat.

  • destroy: Abbau einer eventuell noch vorhandenen Testumgebung, damit der Lauf auf einem sauberen Stand startet.

  • syntax: Syntax-Check des converge.yml (ansible-playbook --syntax-check).

  • create: Provisionierung der Testumgebung (Container, VM, Cloud-Instanz).

  • prepare: Vorbereitungen in der Testumgebung, die nicht zum eigentlichen Test gehören (z.B. SSH-Keys einrichten, python3 nachinstallieren).

  • converge: der eigentliche Test - hier wird die zu testende Automatisierung ausgeführt.

  • idempotence: zweiter converge-Lauf. Meldet Ansible dabei Änderungen, ist die Rolle nicht idempotent.

  • side_effect: separate Automatisierung, die auf unerwünschte Nebeneffekte testet.

  • verify: Validierung expliziter Akzeptanzkriterien, typischerweise mit Testinfra oder Ansible-Assertions.

  • cleanup und destroy zum Abschluss: dieselben beiden Phasen wie am Anfang, jetzt zum Aufräumen nach dem Test.

Der Lauf klammert sich also vorne und hinten mit cleanup und destroy ein. Phasen, für die kein Playbook existiert (z.B. fehlt prepare.yml im Gerüst), überspringt Molecule kommentarlos.

Reihenfolge und Umfang stehen im scenario-Block der molecule.yml (siehe oben). Mit molecule test läuft die komplette Sequenz. Einzelne Phasen lassen sich auch gezielt aufrufen, etwa um Container oder VMs nach converge zu inspizieren, bevor sie gelöscht werden. Jeder Unterbefehl hat dabei seine eigene fest definierte Sequenz: einige ziehen ihre Voraussetzungen automatisch mit, andere führen ausschliesslich ihre namensgebende Phase aus. molecule converge durchläuft dependency, create, prepare und converge; molecule create durchläuft dependency, create und prepare; molecule destroy durchläuft dependency, cleanup und destroy. molecule prepare, molecule verify, molecule idempotence und molecule side_effect führen dagegen nur ihre eigene Phase aus und setzen voraus, dass die Testumgebung bereits existiert (also ein vorheriges molecule create). Ein molecule prepare provisioniert die Instanzen also nicht selbst.

Molecule Cheat Sheet

source ~/venvs/molecule/bin/activate
cd lfops

# full test sequence
molecule test

# only the rhel scenario
molecule test --scenario-name rhel

# step by step, for debugging
molecule create                   # bring up the container
molecule converge                 # apply the role
molecule login                    # log into the container
molecule verify                   # only the verify phase
molecule list                     # show instances and their state
molecule destroy                  # clean up

# verbose output
molecule --debug test

# reset Molecule's internal state after scenario config (molecule.yml) changes
# (test infra should be fully deprovisioned before running this command)
molecule reset

Troubleshooting

Error while fetching server API version oder podman.service: Failed to start

Der Podman-Socket ist nicht erreichbar. Für den aktuellen Benutzer den User-Socket starten: systemctl --user enable --now podman.socket. Im CI-Umfeld zusätzlich DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock exportieren.

Failed to import the required Python library (requests) beim Podman-Driver

Das Python der venv findet den Podman-Client nicht, oder die Abhängigkeiten fehlen. python3 -m pip install 'molecule-plugins[podman]' in der venv erneut installieren.

Container ... not found in converge, obwohl create durchlief

Das Szenario verwendet noch die Platzhalter-create.yml aus molecule init scenario, die keinen Container baut. create.yml und destroy.yml aus dem Szenario löschen, damit die Playbooks des Podman-Drivers greifen (siehe Podman als Driver).

Container ... exited with code 127 bzw. executable file /usr/sbin/init not found

Das Image bringt kein Init-System mit. Statt rockylinux/rockylinux:9 ein Init-Image wie rockylinux/rockylinux:9-ubi-init verwenden.

Molecule bricht mit idempotence: changed tasks ab

Die Rolle ist nicht idempotent: ein zweiter Lauf würde auf der produktiven Maschine erneut Änderungen anwenden. Typische Ursachen sind command/shell ohne changed_when, copy ohne mode:, oder Template-Rendering, das Zeitstempel oder eine zufällige Reihenfolge enthält.

The scenario config file (...) has been modified since the scenario was created am Anfang eines Runs

Molecule ist für interaktive, iterative Nutzung gebaut, die Phasen lassen sich also einzeln aufrufen. Deshalb prüft Molecule zwischen den Aufrufen, ob sich die Szenario-Definition geändert hat. Nach einer Änderung an der molecule.yml muss vor dem nächsten Testlauf molecule reset den internen Zustand zurücksetzen; existiert die Test-Infrastruktur noch, vorher mit molecule destroy aufräumen. Ein molecule destroy allein reicht nicht. Die Warnung ist an dieser Stelle ungünstig bis irreführend formuliert.