ADMIN

2023

06

2023-05-30T12:00:00

Automatisierung

SCHWERPUNKT

078

Automatisierung

Ansible

Event Driven Ansible

Hört auf Zuruf

von Andreas Stolzenberger

Veröffentlicht in Ausgabe 06/2023 - SCHWERPUNKT

Bislang führt Ansible ein Automatisierungs-Playbook nur dann aus, wenn der Anwender es manuell startet. Mit Event Driven Ansible folgt nun eine reaktive Erweiterung, die Automatisierungen ereignisgesteuert ausführt. Wir erklären in diesem Workshop das Regelwerk und zeigen anhand von Beispielen, wie Sie mit EDA Logs überwachen und damit andere Werkzeuge aufrufen.

Dass die Automatisierungsplattform Ansible ohne einen Agenten auf den zu verwaltenden Systemen auskommt, ist zugleich Fluch und Segen. Ohne Client kann Ansible jedes System via SSH und Python oder auch via WinRM und PowerShell direkt steuern, während andere Plattformen wie Puppet oder Salt dafür erst einmal ihren Agenten brauchen. Im Gegenzug können Werkzeuge mit Agenten dann aber zügig auf Vorfälle auf dem verwalteten System reagieren, weil der Client permanent mit der Automatisierungslösung kommuniziert.
Diese Anwendungen arbeiten damit also auch "reaktiv", was Ansible bisher aufgrund der Push-only-Architektur nicht kann. Das neue Projekt "Event Driven Ansible" (EDA) der Community soll das nun ändern, dabei aber nach wie vor auf einen Agenten verzichten.
Modulare Architektur
Das Konzept des ereignisgesteuerten Ansible [1] stellte Red Hat erstmals auf der Anwender- und Entwicklerkonferenz "Ansiblefest" im Herbst 2022 als Developer Preview vor. EDA adaptiert das "State-Machine"-Konzept, wie es unter anderem ManageIQ einsetzt: Eine State-Machine wird von einem "Event" – etwa einer Log-Nachricht namens "Error" – ausgelöst, prüft dann eine oder mehrere "Conditions" – wie "if alerting.service=dns" – und veranlasst dann entsprechende "Actions" – "restart named.service".
Dass die Automatisierungsplattform Ansible ohne einen Agenten auf den zu verwaltenden Systemen auskommt, ist zugleich Fluch und Segen. Ohne Client kann Ansible jedes System via SSH und Python oder auch via WinRM und PowerShell direkt steuern, während andere Plattformen wie Puppet oder Salt dafür erst einmal ihren Agenten brauchen. Im Gegenzug können Werkzeuge mit Agenten dann aber zügig auf Vorfälle auf dem verwalteten System reagieren, weil der Client permanent mit der Automatisierungslösung kommuniziert.
Diese Anwendungen arbeiten damit also auch "reaktiv", was Ansible bisher aufgrund der Push-only-Architektur nicht kann. Das neue Projekt "Event Driven Ansible" (EDA) der Community soll das nun ändern, dabei aber nach wie vor auf einen Agenten verzichten.
Modulare Architektur
Das Konzept des ereignisgesteuerten Ansible [1] stellte Red Hat erstmals auf der Anwender- und Entwicklerkonferenz "Ansiblefest" im Herbst 2022 als Developer Preview vor. EDA adaptiert das "State-Machine"-Konzept, wie es unter anderem ManageIQ einsetzt: Eine State-Machine wird von einem "Event" – etwa einer Log-Nachricht namens "Error" – ausgelöst, prüft dann eine oder mehrere "Conditions" – wie "if alerting.service=dns" – und veranlasst dann entsprechende "Actions" – "restart named.service".
EDA lauscht auf Events von einer oder mehreren "Sources". Wie das reguläre Ansible ist EDA modular aufgebaut und kann verschiedene Sources verwalten. Zum aktuellen Stand gibt es noch wenige dieser Source-Plug-ins, aber EDA befindet sich noch in den Kinderschuhen. Wie Nutzer eigene Source-Plug-ins programmieren können, dokumentiert die EDA-Website [2]. Daher dürfte die Zahl der verfügbaren Quellen nach dem offiziellen Release stetig steigen.
Zu den bereits verfügbaren Plug-ins zählen beispielsweise "ansible.eda.webhook", mit denen sich Ansible in Webapplikationen wie GitHub einklinken kann. Ein Git-Commit in einem Web-Repository kann damit ein Playbook im Rechenzentrum auslösen. Ein sehr mächtiges Source-Plug- in ist "ansible.eda.kafka", mit dem sich EDA in den Message-Bus Apache Kafka einklinkt. Dieses Plug-in setzen wir unter anderem in diesem Workshop ein.
Zudem gibt es schon erste Plug-ins von kommerziellen Herstellern, wie "ansible. eda.dynatrace", "ansible.eda.aws_ cloud­rail" oder "ansible.eda.azure_service_ bus".
Weitere befanden sich Stand Mai 2023 in der Entwicklung, aber noch nicht im aktuellen Beta-Build, wie beispielsweise "ansible.eda.journald", um Systemd-Log-Messages abzuhören. Zum Veröffentlichungstermin dieses Artikels sollte dieses Plug-in aber bereits nutzbar sein.
Das Regelwerk
EDA führt eine neue Variante von Ansibles YAML-basierter Programmiersprache für Playbooks ein: Rulebooks. Diese führt ein ebenfalls neu entwickeltes Programm aus: ansible-rulebook [3]. In den Rulebooks beschreibt der Nutzer, welche Sourcen EDA abhören soll und wie es auf Events reagiert. Rulebooks werden dabei anders als Playbooks gestartet: Eine Rule läuft dauerhaft, wie ein System-Daemon, da sie permanent die angegebene Source überwacht.
In der Praxis dürften Nutzer die Rulebooks in Containern verwenden. Diese enthalten dann neben der Rulebook-Runtime auch den "ansible-runner", um die definierten Action-Playbooks im aktiven Container ausführen zu können. Alternativ spricht das Rulebook die API einer Ansible-Controller- oder AWX-Instanz an, um dort ein Job-Template zu starten. Stand heute arbeitet EDA noch separat von diesen Web-UI-Instanzen. Die Integration ist geplant, erfolgt aber erst nach dem offiziellen Release. Um es vorwegzunehmen: Dieser Workshop fußt noch auf einer Entwickler-Vorabversion von EDA. Einige der hier beschriebenen Verfahren könnten sich mit dem offiziellen Release geändert haben.
Noch zahlreiche Abhängigkeiten
Während das reguläre Ansible-Playbook mit Python zuzüglich einiger Python Libraries auskommt, führt uns der aktuelle Vorab-Build von EDA in gänzlich andere Gefilde der Code- und Runtime-Abhängigkeiten. Denn Python (ab Version 3.9) allein reicht für EDA nicht aus, setzt es doch die freie Business-Rules-Management-Engine "drools" [4] ein. Damit verlangt der EDA-Container neben Python auch die Java-Runtime in der Version 17 für Drools.
Damit der Python-Teil von EDA mit seinem Java-Teil kommunizieren kann, bedarf es zudem noch der Java-to-Python-Bridge "jpy" [5]. Und um jpy in dem Container zu kompilieren, braucht es dann das Software-Project-Management "Maven" – wessen RPM-Paket in EL8 aber die alte Java-Runtime 1.8 statt 17 erfordert. Somit stecken im EDA-Developer-Container sowohl Python 3.9 samt Devel-Environment, gcc, make und zwei Java-Runtimes. Auch das dürfte sich bis zum Release noch ändern.
Zum Zeitpunkt der Erstellung dieses Workshops gab es zum Glück bereits ein fertiges Container Image für EDA-Tests auf "quay.io/ansible/ansible-rulebook: main". Wichtig ist dabei der Tag "main". Das Image mit dem Tag "latest" verwendet eine noch ältere Version.
Logüberwachung per Message Bus
Für unseren Workshop setzen wir ein praktisches Beispiel ein, bei dem EDA die Systemlogs mehrerer Server überwacht und auf verschiedenste Logeinträge reagiert. Um an die Logs heranzukommen, könnte EDA das Source-Plug-in "ansible. eda.journald" nutzen. Dazu müsste es dann aber aktive Verbindungen zu allen überwachten Servern offenhalten. Vielerorts sammeln Administratoren ohnehin alle Logfiles in einer zentralen Datenbank.
Damit die Logdaten dabei nicht nur einem einzigen Dienst wie Logstash zur Verfügung stehen, lässt sich dabei – in bester Scale-out-Manier – ein Message-Bus dazwischenschalten: Apache Kafka. Alle Loginformationen passieren den Message-Broker und Zielsysteme wie die Log-Collection-Datenbank und auch EDA schaltet sich als "Subscriber" auf den Nachrichten-Bus.
Wem diese Message-Bus-Technik noch nicht geläufig ist, der stelle sich Kafka einfach wie WhatsApp für Applikationen vor. Die loggenden Systeme treten dabei einer Chatgruppe (Topic) bei und posten alle Logeinträge in dieser Gruppe. Nun können auch "Subscriber" wie EDA der Gruppe beitreten, alle Nachrichten einsehen und auswerten. Parallel kann ein Logaggregator wie Logstash alle Nachrichten der Gruppe abrufen und an eine Datenbank wie Elasticsearch zur Archivierung weiterleiten.
Im Beispiel läuft zunächst das Tool Filebeat [6] als Log-Shipper auf den zu überwachenden Servern. Es kümmert sich darum, dass sich unstrukturierte Logeinträge der Systemdienste in halbwegs strukturierte Nachrichten wie "Dienst=sshd" oder "Message=Failed Login" verwandeln, bevor Filebeat sie weiterleitet. Das Tool cached zudem alle Nachrichten, sollte die Verbindung zum Message-Bus abbrechen, damit nichts verloren geht.
In vielen Installationen sendet Filebeat die Daten direkt zu Logstash – damit stehen sie anderen Diensten nicht zur Verfügung. Die Umstellung auf den Message-Bus als Mittelsmann ist jedoch kein großer Aufwand, im Gegenteil.
Das ergibt besonders in verteilten, hybriden Umgebungen mit mehreren Clouds und Setups im Rechenzentrum Sinn, da es die benötigten Verbindungen zwischen den separaten Netzwerken reduziert. Aus "Filebeat -> Logstash" wird die Folge "Filebeat -> Kafka -> Logstash", mit der Option, dass andere Dienste bei Kafka mithören.
Bild 1: Logstash kann die Logs vom Kafka-Mesage-Bus abrufen und in Elasticsearch sichern.
Von Filebeat zu Kafka
Während große Installationen Kafka als Cluster betreiben, reicht für unser Testsetup ein einzelner Container aus. Sie können optional die rohen Nachrichten des Busses auf einem persistenten Volume protokollieren. In der Regel landen alle Nachrichten aber ohnehin in einem Logaggregator wie Logstash , weswegen der Kafka-Container auch gut stateless, also ohne dauerhaften Speicher, funktioniert.
Für Kafka gibt es ein vorgefertigtes Container-Template "docker.io/bitnami/kafka". Folgen Sie der Anleitung auf der Docker-Seite des Images [7] und nutzen Sie das Setup aus dem Abschnitt: "Kafka without Zookeeper". Letzteres kommt nur bei Clustern zum Einsatz, weswegen für den Single-Node-Betrieb "Kafka Raft" als Quorum genügt. Anders als im Beispiel angegeben, tragen sie jedoch die externe IP-Adresse ihres Servers ein, auf dem sie den Kafka-Container mit Docker oder Podman laufen lassen. Statt
environment:
- KAFKA_CFG_ADVERTISED_LISTNERS= PLAINTEXT://127.0.0.1:9092
steht dort also beispielsweise
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://192.168.2.12:9092
wenn Ihr Docker/Podman-Server auf 192.168.2.12 läuft. In einer produktiven Umgebung läuft das Kafka-Setup dann natürlich ohne "Plaintext" und erlaubt nur verschlüsselte Verbindungen mit Autorisierung. Sobald der Kafka-Container läuft, konfigurieren Sie die Filebeat-Clients auf den Servern, die ihre Logs-Informationen an Kafka senden sollen. Die jeweilige Datei "/etc/filebeat/filebeat.yml" sieht dann in etwa so aus:
filebeat.inputs:
- type: journald
    id: everything
 
output.kafka:
    hosts: ["192.168.2.12:9092"]
    topic: 'journals'
    partition.round_robin:
    reachable_only: false
Hier sendet Filebeat alle Lognachrichten an den Message Broker Topic "journals". Alternativ könnte Filebeat Messages je nach Inhalt an verschiedene Topics senden, wie beispielsweise mit folgendem, optionalen Eintrag:
topics:
- topic: "critical"
when.contains:
message: "CRITICAL"
Sobald Filebeat Logdaten sendet, können Sie auf dem Host mit dem Kafka-Container prüfen, ob die Informationen am Message-Bus korrekt ankommen. Dazu führen Sie im laufenden Kafka-Container "kafka" einfach den console-Consumer aus:
podman exec -it kafka \
kafka-console-consumer.sh \
--bootstrap-server localhost:9092 \
--topic journals --from-beginning
Nun sollten Sie die JSON-formatierten Log-Einträge Ihrer Systeme mit Filebeat-Setup sehen. Wollen sie optional alle Loginformationen via Logstash einsammeln, reichen dazu folgende Zeilen in der Log­stash-Konfiguration:
input {
kafka{
codec => json
bootstrap_servers => "192.168.2.12:9092"
topics => ["journals"]
}
}
Bild 2: Der Event "Stopped DNS" vom Kafka-Messagebus startet die Ansible-Regel. Das getriggerte Playbook fährt den ausgefallenen Dienst wieder hoch.
Inventory anlegen
In einem Terminal starten Sie den EDA-Container interaktiv via:
podman run \
-it \
--name eda \
--volume /home/user/rules:/ rules:Z \
quay.io/ansible/ansible-rulebook:main \
/bin/bash
Jetzt können Sie auf dem Host-Rechner die Rule- und Playbooks bearbeiten und im interaktiven Container testen. Das funktioniert im Übrigen sogar unter Windows, wenn Sie eine WSL-Distribution mit Podman verwenden.
Unter "/home/user/rules" legen Sie zuerst ein Inventory an. EDA erlaubt aktuell nur das YAML-Format für Inventories und nicht das alte INI-Format. In unserem Test liefern zwei Systeme Loginformationen an Kafka, daher sieht unsere "inventory.yml" so aus wie in Listing 1.
Listing 1: Datei "inventory.yml"
all:
    children:
       servers:
          hosts:
             srv1.local.ip:
                ansible_host: 192.168.2.1
             srv2.local.ip:
                ansible_host: 192.168.2.2
    vars:
       ansible_user: root
Für unsere Tests meldet sich Ansible als root-User an den Remotesystemen an. In einer produktiven Umgebung stünde hier ein Directory-User mit Sudo-Rechten und das Playbook würde "become" verwenden.
Die Hosts listen wir mit ihren FQDN im Inventory auf, weil diese so in den Logeinträgen auf dem Kafka-Bus stehen und sich damit dem Inventory zuweisen lassen. Für die Ansible-Verbindung geben wir jedoch die IP-Adresse an. Das Beispiel des Workshops zeigt nämlich, wie EDA bei einem DNS-Ausfall reagieren kann – und da muss es den Host natürlich über die IP-Adresse kontaktieren.
Regeln setzen
Das aufgeführte Testsetup kann prinzipiell auf jeden Logeintrag reagieren und ein dazu passendes Ansible-Playbook laufen lassen. Unser Beispiel startet den DNS/ DHCP-Server "dnsmasq" neu, falls dieser aus irgendwelchen Gründen stoppt. Dazu überwacht EDA die Lognachrichten von Systemd. Das Rulebook "rule_dns.yml" startet wie in Listing 2 dargestellt mit der Kafka-Source.
Listing 2: Rulebook "rule_dns.yml"
- name: Kafka Monitor
    hosts: all
    sources:
        - name: Kafka
          ansible.eda.kafka:
             host: 192.168.2.12
             port: 9092
             topic: journals
Damit wird sich ansible-rulebook später in den Message-Bus einklinken und eingehende Nachrichten in einer hierarchischen Variable "events" weiterleiten. Zu Sourcen gibt es auch "Filter", die den Inhalt oder die Struktur der Variablen ändern können. Ein Filter könnte so aussehen:
filters:
    - json_filter:
       exclude_keys: ['user']
Er würde beispielsweise alle Werte "event.user" aus dem Source-event entfernen. Ein wichtiger Filter ist dabei "insert_hosts_to_meta". Dieser übernimmt einen oder mehrere Host-Namen aus der Source-Nachricht und verwendet diese bei der Ausführung eines Playbooks dann als "limit". Ein via EDA gestartetes Ansible-Playbook kontaktiert somit nur diejenigen Hosts, die "insert_hosts_to_meta" zuvor in die Variable "event.meta.host" übertragen hat. In diesem Workshop kommt der Filter jedoch nicht zum Einsatz – einfach weil er in dem verwendeten Test-Build 0.11 von ansible-rulebook noch nicht enthalten war.
Die Testregel reagiert auf die Nachricht, dass der DNS-Server angehalten wurde:
rules:
    - name: Monitor DNS Service
             condition: event.message is search("Stopped DNS", ignorecase=true)
Sollte eine Nachricht auf dem Message-Bus nun "Stopped DNS" enthalten, wird EDA tätig und führt eine Action aus:
action:
    run_playbook:
       name: start_dns.yml
       extra_vars:
          event_host:
             "{{ event.host.name }}"
Im Beispiel beginnt EDA nur eine "Action". Das Rulebook könnte alternativ das Keyword "Actions" einsetzen und dann mehrere Aktionen nacheinander ausführen. Da die komplette Event-Variable des Rulebooks nicht automatisch für das Playbook zur Verfügung steht, müssen Sie Informationen aus der Rulebook-Variable über "extra_vars" in das Playbook hineinreichen.
Das referenzierte Playbook startet den DNS-Server, muss dazu aber wissen, auf welchem Host der Dienst zu starten ist. Da wie oben erwähnt der "limit"-Filter zum Zeitpunkt der Workshoperstellung nicht funktioniert, setzen wir also stattdessen einen simplen Trick im Playbook ein. Das start_dns.yml-Playbook sieht daher so aus wie in Listing 3. Es übernimmt als Host-Variable einfach den Wert aus dem Event und führt damit die Aktionen nur auf dem Host aus, der den Event ausgelöst hat.
Listing 3: Playbook "start_dns.yml"
- hosts: "{{ event_host }}"
    gather_facts: no
    tasks:
        - name: Start DNS Service
          ansible.builtin.service:
             name: dnsmasq
             state: started
Simpel mit großem Potenzial
Das aufgeführte Beispiel ist recht einfach gehalten, zeigt aber das große Potenzial von EDA auf. Das verwendete Rulebook ist dabei natürlich nicht auf eine Regel beschränkt. Sie können mit weiteren Regeln auf Kombinationen von Source-Events reagieren und die Bedingungen an mehrere Sourcen knüpfen (and / or), in etwas so:
condition:
    all:
       - event.host.name == "srv1.local.ip"
          - event.journald.process.name == "systemd"
          - event.systemd.unit == "dnsmasq.service"
          - event.message is search("Stopped DNS", ignorecase=true)
Hier löst der Event nur aus, wenn alle der angegebenen Werte passen (and). Das Keyword "any" startet die Aktion, wenn eine der angegebenen Konditionen zutrifft (or) . Auch bei den Aktionen gibt es weitere Optionen. Während Sie eigene Rulebooks entwickeln, werden Sie häufig auf "print_event" zugreifen, um darüber die komplette Event-Variable oder Teile davon einsehen zu können und ihre Regeln und Playbooks entsprechend anzupassen.
Die Aktion "start_playbook" führt ein Playbook via Ansible-Runner im EDA-Container aus.
In Zukunft wird es jedoch viel interessanter, ein bestehendes Job-Template auf einem Ansible-Controller oder AWX-Setup zu starten. Dazu gibt es die Aktion "run_ job_template", die via URL und Token die Verbindung zu einem bestehenden Controller/AWX-System aufnimmt. Über kurz oder lang dürfte EDA in den Web-UIs Controller und AWX Einzug halten und zu einem Teil dieser Tools werden.
Fazit
Die Architektur von Event Driven Ansible führt das Konzept fort, Zielsysteme ohne einen Agenten zu automatisieren. Allerdings gestaltet sich die praktische Umsetzung im derzeitigen frühen Stadium noch etwas kompliziert. Drools ist eine mächtige Rules-Engine und mit den bisherigen, simplen Möglichkeiten von EDA eher unterfordert. Daher stellt sich zwangsweise die Frage, ob EDA den Aufwand mit Python, Java und der jpy-Bridge wirklich benötigt.
Auf der anderen Seite dürfte EDA in künftigen Versionen mächtig an Funktionen zulegen, gerade weil es auf einer so leistungsstarken Rules-Engine basiert. Das modulare, offene Konzept verspricht, dass sich EDA künftig in sehr vielen Szenarien mit vielen unterschiedlichen Sources einsetzen lässt – sofern Nutzer und Entwickler das Tool weiterentwickeln und weitere Source-Plug-ins liefern.
(ln)
Link-Codes