Wenn man ansible einsetzt, kommt man schnell an den Punkt, an dem man um Rollen nicht mehr herum kommt. Die Rolle wird dann immer weiter entwickelt und kommt in immer mehr Projekten zum Einsatz. Und schon sind die Auswirkungen von kleinen Änderungen in anderen Projekten schwer nachvollziehbar. Spätestens ab hier führt kein Weg mehr an einem vernünftigen Testen der Rolle vorbei.

Auch sind manche Rollen „fertig“ und werden über einen längeren Zeitraum nicht mehr geändert, aber weiterhin in Projekten verwendet. Ohne Tests passiert es schnell, dass die Rolle mit einer neuen ansible- oder Betriebssystem-Version nicht mehr kompatibel ist. Auch hier kann ein regelmäßiges, automatisches Testen der Rolle das Schlimmste verhindern.

Testframeworks

Wenn man sich nach Tools zum Testen von ansible playbooks umschaut, merkt man schnell, dass es keine große Auswahl gibt. Es gibt Test-Frameworks für andere Konfigurationsmanagment-Tools (z.B. Test Kitchen für Chef) mit Adaptern für ansible, verschiedene ansible-Rollen, die beim Testen unterstützen (Chris Meyers, Five Questions: Testing Ansible Playbooks & Roles), aber eine wirklich gute Software war bisher nicht dabei.

Dann fand ich Molecule, ein Test-Framework, dass von Beginn an für ansible entwickelt wurde.

Einführung in Molecule

Bevor man mit Molecule startet sollte man sich die Funktionsweise und die Begrifflichkeiten verdeutlichen.

Begriffe

In Molecule verwendet man den sogenannten Driver, um die Instanzen zum Testen bereit zu stellen. Man kann hier Docker, Podman, Vagrant oder auch die Public Clouds Azure, AWS und einige mehr verwenden. In der Dokumentation findet man alle unterstützen Technologien.

Die nächste wichtige Komponente sind die verschiedenen Verifier. Das sind die Tools, die am Ende das Ergebnis auf den Instanzen prüfen. Hier kann man sich entscheiden zwischen Inspec, Testinfra, Goss und ansible selbst.

In Molecule verwendet man ein sogenanntes Scenario, um einen bestimmten Anwendungsfall zu testen. Weitere Szenarien für weitere Anwendungsfälle können einfach hinzugefügt werden. Jedes Szenario kann eigene Plattformen, Driver und Verifier verwenden. Zum Beispiel habe ich oft ein Szenario „install“ und ein Szenario „upgrade“.

Test-Sequenz

Wenn man in einem Szenario einen Testlauf startet, sieht man die folgende Test-Matrix:

  • dependency
  • lint
  • cleanup
  • destroy
  • syntax
  • create
  • prepare
  • converge
  • idempotence
  • side_effect
  • verify
  • cleanup
  • destroy

Dies sind die verschiedenen Schritte, die in einer Test-Sequenz durchlaufen werden können. Manche davon werden automtisch übersprungen, wenn keine Konfiguration für den jeweiligen Schritt vorhanden ist, manche kann man manuell deaktivieren. Die wichtigsten Schritte will ich kurz erläutern:

  • dependency: Hier werden Abhängigkeiten geladen (andere Rollen oder Collections, die von dieser Rolle benötigt werden). Ob hier Ansible Galaxy verwendet wird oder ein anderer Dienst kann man in der Datei molecule.yml konfigurieren.
  • lint: Die verschiedenen Dateien werden auf die korrekte Syntax geprüft.
  • destroy: Die erstellten Instanzen werden zerstört. Dies geschieht insgesamt zwei Mal (einmal am Anfang der Tests und einmal am Ende), um sicher zu gehen, dass alle Instanzen zerstört sind und keine Überreste aus vergangenen Testläufen das Ergebnis beeinflussen.
  • prepare: Erstellt man eine Datei prepare.yml im Ordner des jeweiligen Szenarios, so werden diese Anweisungen auf die Instanzen vor der Ausführung der eigentlichen Rolle angewandt. So können die Instanzen vorbereitet werden für die verschiedenen Anwendungsfälle, es kann zum Beispiel eine alte Version installiert werden, um ein Upgrade zu testen. Es handelt sich bei der Datei prepare.yml um ein ansible Playbook, in dem man einfach die notwendigen Tasks auflistet.
  • converge: Die zu testende Rolle wird auf allen Instanzen ausgeführt.
  • verify: Die Tests werden ausgeführt und das Ergebnis auf den Instanzen geprüft.

Testen mit Molecule

Installation

Die Installation ist in der Dokumentation beschrieben.

Initialisierung

Bei Molecule gibt es zwei Initialisierungsarten. Um Molecule in einer bestehenden Rolle zu initialisieren wechselt man in das Verzeichnis der Rolle und initialisiert ein Szenario:

molecule init scenario --driver-name docker --verifier-name testinfra --role-name nginx

Um eine neue Rolle zu beginnen initialisiert man eine Rolle mit dem folgenden Befehl:

molecule init role --driver-name docker --verifier-name testinfra nginx

Mit diesem Befehl wird die gleiche Ordnerstruktur erzeugt, die auch mit dem Tool ansible-galaxy erstellt werden kann, plus einem zusätzlichen Ordner molecule.

Hier die Rolle nginx mit den bereits eingefügten Tasks und Handlern (nginx.tar.gz), damit sind die weiteren Schritte nachvollziehbar.

Testen

Als erstes öffne ich im Ordner molecule/default die Datei molecule.yml. Dort entferne ich den dependency-Eintrag, da er bei dieser Rolle nicht benötigt wird. Dann aktiviere ich nur die Schritte in der Test-Sequenz, die hier benötigt werden:

scenario:
  name: default
  test_sequence:
    - lint
    - destroy
    - syntax
    - create
    - converge
    - idempotence
    - verify
    - destroy

Ich aktiviere das Linten:

lint: |
  yamllint .
  ansible-lint .

Und zum Schluss aktiviere ich die von mir gewünschten Plattformen.

platforms:
  - name: ubuntu-focal
    image: ubuntu:focal
  - name: ubuntu-bionic
    image: ubuntu:bionic
  - name: debian-buster
    image: debian:buster
  - name: debian-stretch
    image: debian:stretch

Und jetzt wird getestet. Im Hauptordner nginx startet man molecule.

molecule test

Nachdem die Test-Sequenz erfolgreich durchgelaufen ist wurden 4 Tests erfolgreich abgeschlossen.

Sollte ein Schritt in der Sequenz fehlschlagen, kann man ihn auch einzeln wiederholen ohne die gesamte Test-Sequenz zu starten. Zum Beispiel möchte ich zu Beginn nur die Code-Syntax prüfen:

molecule lint

Oder wenn man mal in die Docker-Container rein schauen will, dann startet man am besten den Schritt converge.

molecule converge

Der Schritt destroy wird hier nicht ausgeführt, so dass die Container am Ende noch vorhanden sind und man sich das in den Containern erzeugte Ergebnis im Detail anschauen kann.

Ergebnisse prüfen

Wenn der Webserver nginx über die ansible-Rolle korrekt installiert wurde, dann

  • ist das Paket nginx installiert
  • gibt es einen Dienst namens nginx, der aktiviert ist und läuft
  • wurde die Datei /var/www/html/index.html erstellt
  • hört ein Dienst auf Port 80

Diese Annahmen werden wir nun automatisiert testen. In der Datei molecule.yml ist der Verifier wie folgt festgelegt:

verifier:
  name: testinfra

Testinfra ist ein in Python geschriebenes Testframework, damit testen wir die oben genannten Annahmen. In der Datei molecule/default/test/test_default.py fügen wir die folgenden Tests ein:

def test_nginx_installed(host):
  """Validate that the nginx package is installed."""
  nginx = host.package("nginx")

  assert nginx.is_installed

def test_nginx_running_and_enabled(host):
  """Validate that the nginx service is enabled and running."""
  nginx_service = host.service("nginx")

  assert nginx_service.is_running
  assert nginx_service.is_enabled

def test_nginx_listening(host):
  """Validate that a service is listening on port 80."""
  assert host.socket('tcp://127.0.0.1:80').is_listening

def test_html_file(host):
  """Validate /var/www/html/index.html is present."""
  f = host.file("/var/www/html/index.html")

  assert f.exists
  assert f.user == "www-data"
  assert f.group == "www-data"

Jetzt kann man eine neue Test-Sequenz starten und die Rolle wird erfolgreich getestet. Hier nochmal das finale Paket mit allen hier genannten Anpassungen und Tests: nginx_with_molecule.tar.gz

Viel Spass beim Testen eurer ansible-Rollen!

Nächster Beitrag