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...
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]
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! 🙂