Das erste Event-driven Ansible-Plugin

Während der Red Hat Summit im Mai diesen Jahres wurde mit Event-driven Ansible eine interessante Erweiterung der beliebten Infrastructure as Code-Lösung vorgestellt. Mit EDA wird Ansible um eine proaktive Komponente erweitert, welche es erlaubt dynamisch auf Ereignisse zu reagieren. Damit wird das bisherige Alleinstellungsmerkmal von SaltStack (Event-driven infrastructure) fokussiert.

Zentraler Bestandteil der Erweiterung sind sogenannte Rulebooks, in welchen zu überwachende Ereignisse definiert werden. Tritt ein solches Event ein, kann eine entsprechende Gegenmaßnahme definiert werden - beispielsweise das Ausführen von Playbooks. Für die Überwachung werden entsprechende Event Source-Plugins benötigt. Mit einer überschaubaren Anzahl an Plugins ist die Vielfalt an Möglichkeiten derzeit noch etwas limitiert und kann sich noch nicht ganz am ausgereifteren SaltStack EDI messen. Infrastruktur-nahe Plugins, beispielsweise für lokale Services oder Festplattenauslastungen sucht man noch vergebens.

Event-driven Ansible hat mit Version 2.4 Einzug in Ansible Automation Platform (AAP) erhalten. Die Ausführungslogik wird dort in einem Decision Environment (DEs) gekapselt - analog zu den ähnlich aufgebauten Execution Environments (EEs). Es handelt sich hierbei um Podman-Container auf Basis von RHEL oder CentOS Stream mit installiertem Ansible sowie benötigten Collections. Diese Container werden dann beim Ausführen von Ansible-Logik über den Automation Controller (EEs) bzw. EDA Controller (DEs) gestartet.

Installation

Gemäß Dokumentation müssen neben Java auch Python-Pakete installiert werden - beispielsweise unter Fedora:

1# dnf install java-17-openjdk python3-pip
2$ pip3 install --user ansible-rulebook ansible-runner

In meinem Fall fehlte zusätzlich noch das Python-Modul watchdog:

1$ pip3 install --user watchdog

Wichtig ist auch die dazugehörige Ansible-Collection:

1$ ansible-galaxy collection install ansible.eda

Als ersten Test lässt sich schnell ein lokaler Webserver mit Python starten und mit einem Rulebook überwachen.

Zum Starten des Servers genügt folgender Aufruf:

1$ python3 -m http.server
2Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Anschließend lauscht dieser auf Port 8080. Ein einfaches Rulebook könnte folgendermaßen aussehen:

local_webserver_rulebook.yml

 1---
 2- name: Listen for events on a web service
 3  hosts: localhost
 4  # define source plugin
 5  sources:
 6     - ansible.eda.url_check:
 7        urls:
 8          - http://localhost:8000
 9        delay: 10
10
11  rules:
12    # define condition
13    - name: Website is up
14      condition: event.url_check.status == "up"
15    # define action
16      action:
17        run_module:
18          name: ansible.builtin.debug
19          module_args:
20            msg: "Website is up, all good!"
21
22    - name: Website is down
23      condition: event.url_check.status == "down"
24      action:
25        run_module:
26          name: ansible.builtin.debug
27          module_args:
28            msg: "Oh noes, website is down :("

Es fehlt noch ein Inventory - der Einfachheit werden hier nur Debug-Ausgaben vorgenommen und keine Playbooks ausgeführt. Daher tut es ein einfaches localhost-Inventory:

localhost-inventory.ini

1localhost

Ausgeführt wird das Ganze nun wie folgt in einem weiteren Terminal:

1$ ansible-rulebook -i localhost-inventory.ini --rulebook local_webserver_rulebook.yml

Alle 10 Sekunden wird nun auf Erreichbarkeit des Webservers überprüft - bricht man den Webserver nach einigen Sekunden diesen mit STRG+C ab, sieht das beispielsweise so aus:

 1...
 2PLAY [wrapper] *****************************************************************
 3
 4TASK [Module wrapper] **********************************************************
 5ok: [localhost] => {
 6    "msg": "Website is up, all good!"
 7}
 8
 9TASK [save result] *************************************************************
10ok: [localhost]
11
12...
13
14PLAY [wrapper] *****************************************************************
15
16TASK [Module wrapper] **********************************************************
17ok: [localhost] => {
18    "msg": "Oh noes, website is down :("
19}
20
21TASK [save result] *************************************************************
22ok: [localhost]
23...
Hinweis

Auf folgender Webseite kann EDA auch online ausprobiert werden.

Entwicklung eigener Plugins

In der Collection ansible.eda finden sich einige Beispiele, an denen man sich bei der Entwicklung orientieren kann. Die Entwicklungsdokumentation ist derzeit recht dünn und eindeutig verbesserungswürdig.

Bei einem kürzlichen Ansible-Hackathon kam mir gleich die Idee, die Uyuni Ansible-Collection um entsprechende proaktive Funktionen zu erweitern. Diese verwendete bisher immer noch Python-Code, der vor langer Zeit im katprep-Projekt entwickelt wurde. Die entsprechende Bibliothek wurde bisher mittels relativem Import als Modul-Dienstprogramm (module_utils/uyuni.py) genutzt. Genau das scheint mit EDA aufgrund eines Bugs aber nicht zu funktionieren.

Ärgerlich, aber ich wollte den Code ohnehin mal aufräumen und in eine dedizierte Bibliothek auslagern - also, danke für die extrinsische Motivation. 🫠

Um eine Collection um EDA-Funktionalität zu ergänzen, genügt es die folgende Ordnerstruktur anzulegen:

1.
2└── extensions
3    └── eda
4        ├── plugins
5        │   ├── event_filter
6        │   └── event_source
7        │       ├── XXX.py
8        └── rulebooks
9            └── XXX.yml

Wie schon vermutet, finden sich innerhalb des plugins-Ordners die Event Source-Plugins, während der rulebooks-Ordner die entsprechenden Rulebooks enthält. Für das Filtern von Informationen gibt es einen eigenen Plugin-Typ, der dazugehörige Ordnername lautet plugins/event_filter. Ein guter Einstiegspunkt ist der offizielle Beispielcode auf GitHub.

Ich habe innerhalb ein paar Stunden ein Event Source-Plugin requires_reboot.py entwickelt. Dieses gibt zurück, ob ein spezifisches System einen Reboot benötigt - beispielsweise nach der Installation eines Kernel-Updates. Zugegebenermaßen kein sonderlich wichtiger Anwendungsfall - aber darum geht es in Hackathons ja nicht notwendigerweise. 🙂

Um das Plugin anzuwenden, fehlt noch ein Rulebook:

trigger_reboots.yml

 1---
 2- name: Rebooting hosts
 3  hosts: localhost
 4  gather_facts: false
 5  tasks:
 6    - name: Show system that will be rebooted
 7      ansible.builtin.debug:
 8        msg: "Host to be rebooted: {{ ansible_eda.event.host }}"
 9
10    - name: Reboot system
11      stdevel.uyuni.reboot_host:
12        uyuni_host: 192.168.1.10
13        uyuni_user: admin
14        uyuni_password: admin
15        uyuni_verify_ssl:
16        name: "{{ ansible_eda.event.host }}"

Damit dieses ausgeführt werden kann, fehlt noch ein Inventory. Ausgeführt wird das Ganze dann wie folgt:

1$ ansible-rulebook -i inventory.ini --rulebook trigger_reboots.yml

Wenn man nun ein Update installiert, welches einen Reboot benötigt, erkennt EDA dies und steuert gegen:

 1checking host uyuni-client.xxx.de
 2
 3PLAY [Rebooting hosts] *********************************************************
 4
 5TASK [Show system that will be rebooted] ***************************************
 6ok: [localhost] => {
 7    "msg": "Host to be rebooted: uyuni-client.xxx.de"
 8}
 9
10TASK [Reboot system] ***********************************************************
11changed: [localhost]

Event-driven Ansible bootet einen über Uuyni verwalteten Host nach Wartungsarbeiten

Cool, oder? 🙂

Ausblick

Der aktuelle Entwicklungsstand kann im entsprechenden Pull Request beobachtet werden - weitere Use Cases sollen in Zukunft folgen. Hier freue ich mich jederzeit über Feedback und konstruktive Kritik. Sowohl die Python-Bibliothek pyuyuni als auch die EDA-Plugins befinden sich in einem frühen Entwicklungsstadium.

Denkbar wären beispielsweise:

  • auf nicht mehr reagierende Hosts reagieren (z.B. Salt-Minion neustarten)
  • auf entsprechende System-Events reagieren (z.B. durch OpenSCAP als unsicher spezifizierte Systeme abhärten)

Es bleibt also spannend! 🙂

Übersetzungen: