Ansible ist aus der Linux-Welt nicht mehr wegzudenken. Das Tool ermöglicht dem Administrator nämlich die parallele Ausführung von Arbeitsschritten auf mehreren Hosts zur gleichen Zeit. Dabei kann man sogar Fehlerbehandlungen in die Playbooks einbauen und mit APIs kommunizieren. Das wird besonders dann interessant, wenn man Downtimes setzen möchte oder neue VMs ausrollen will.
Darüber hinaus kann Ansible aber noch weit mehr. Da wären noch die Collections und Rollen sowie einer Vielzahl von Drittanbieter-Tools, die über Ansible Galaxy verfügbar sind. Alles in einem kann man damit die vorhandene Automatisierung erweitern, indem man andere Systeme via API anspricht. Ich selbst nutze diese, um virtuelle Maschinen zu stoppen und im Anschluss ein Backup zu erstellen.
In diesem Beitrag möchte ich dir zeigen, wie schnell und einfach man APIs in Playbooks einbinden kann. Dies ist besonders nützlich, da die Anzahl der REST-APIs in rasantem Tempo zunimmt. Tatsächlich gibt es ein öffentliches GitHub-Repository mit Hunderten von kostenlosen APIs in mehr als einem Dutzend Kategorien, das allein schon den Umfang der Möglichkeiten zeigt.
Welche APIs können mit Ansible verwendet werden?
Mit Ansible kann man im Grunde betrachtet jede REST-API ansprechen, die eine URL-Endpunkt-Struktur verwendet. REST-APIs sind eine weit verbreitete Möglichkeit, um auf Daten und Funktionalitäten von Webdiensten zuzugreifen und können in fast allen Programmiersprachen implementiert werden. Vielleicht nutzt du sogar schon API-Calls in Shell-Skripten mittels Curl.
Einige Beispiele für APIs, die mit Ansible eingesetzt werden können, sind:
- Cloud-Provider-APIs wie Amazon Web Services, Google Cloud Platform und Microsoft Azure
- Infrastruktur-APIs wie OpenStack, VMware und Cisco
- Container-Orchestrierungs-APIs wie Kubernetes und Docker Swarm
- Monitoring-APIs wie CheckMK, Nagios und Zabbix
- Konfigurationsmanagement-Tool-APIs wie Puppet, Chef und SaltStack
Wie du siehst, sind die Möglichkeiten nahezu unbegrenzt. Ansible ist äußerst flexibel und ermöglicht es dir so nahezu jede REST-API nutzen zu können. Wichtig zu wissen wäre dabei noch, dass du dich bei den meisten API-Endpunkten authentifizieren musst. Häufig brauchst du also einen Token, bestehend aus Passwort und Nutzername. Sicherheit geht schließlich vor.
Wie schaut ein einfaches Playbook mit API-Call aus?
Die einfachste Variante, um mit einer API zu interagieren, ist eine GET-Anfrage. Damit kannst du bestimmte Informationen abfragen, wie den Fortschritt eines Backups oder die ID einer virtuellen Maschine bei Proxmox. Ein Playbook für eine solche Anfrage mit dem URI-Modul würde so aussehen:
- name: GET
hosts: localhost
connection: local
gather_facts: no
vars:
api_endpoint: https://api.example.com/data
api_key: <your_api_key>
tasks:
- name: Send API call
uri:
url: "{{ api_endpoint }}"
method: GET
headers:
Authorization: "{{ api_key }}"
return_content: yes
register: api_response
- name: Show response from API
debug:
var: api_response.content
Natürlich kann ein Ansible-Playbook für eine API je nach Bedarf und Anforderungen sehr unterschiedlich ausschauen. So kannst du beispielsweise auch POST-, PUT- oder DELETE-Anfragen senden. Diese sind dann interessant, wenn du zur Interaktion mit der API Daten übermitteln musst und im Anschluss etwas ausgeführt werden soll. Doch was heißt das konkret?
Mit einer POST-Anfrage kannst du zum Beispiel neue Benutzer für deine Anwendung anlegen. Mithilfe von einer DELETE-Anfrage kannst du Ressourcen bei deinem Cloud-Provider löschen. Mittels PUT verändert man gewöhnlich Ressourcen. Und eine GET-Anfrage fordert ausschließlich Daten an, wie zum Beispiel die Anzahl der Repliken eines Containers.
Die Vorgehensweise mit dem URI-Modul unterscheidet sich nicht bei PUT-, DELETE- oder POST-Anfragen. Lediglich bei einem GET kann man einen Unterschied feststellen. Hier gibt es für gewöhnlich keinen Body, da man nur Informationen ausliest und keine Interaktion anstößt.
Nachdem wir nun geklärt haben, welche Anfragen man an eine API übermitteln kann und was diese dann genau tun, kommen wir nun noch zu ein paar Beispielen. Ich werde dazu auf das URI-Modul zurückgreifen und möglichst generisch arbeiten, damit der Code leicht verständlich ist. Und keine Sorge, in den meisten API-Dokus ist genau beschrieben, welcher Anfrage-Typ samt Daten erwartet wird.
POST-Anfrage zum Erstellen von Benutzern:
Um neue Nutzer via API erstellen zu können, braucht man eine POST-Anfrage. Im Header authentifiziert man sich gegenüber der API und im Body wird der Payload mitgesendet. Das sind in diesem Fall der Nutzername, das Passwort sowie die Mailadresse. Übermittelt werden sollen die Daten im gängigen JSON-Format. Daher wird dies für den Body noch spezifiziert.
- name: POST
hosts: webservers
become: yes
vars:
api_endpoint: https://api.example.com/users
api_key: <your_api_key>
tasks:
- name: Create users
uri:
url: "{{ api_endpoint }}"
method: POST
headers:
Authorization: "{{ api_key }}"
return_content: yes
body_format: json
body:
username: user1
password: secret123
email: user1@example.com
register: api_response
- name: Show response from API
debug:
var: api_response.content
Sollte es bei der Ausführung zu Fehlern kommen, reagiert das Playbook darauf nur unzureichend. Deshalb sollte man noch den HTTP-Status-Code mit dem URI-Modul überwachen. Dies geht ganz einfach, in dem man noch die Direktive status_code: 201 nutzt. Welcher Code dabei genau für einen erfolgreichen API-Call steht, kann man in der jeweiligen Dokumentation nachlesen.
PUT-Anfrage zum Aktualisieren von Konfigurationsdateien:
Der folgende Code unterscheidet sich kaum vom Beispiel weiter oben. Allerdings wird der Inhalt des Body hier nicht als JSON-Objekt übermittelt. Die Direktive body_format: raw im Ansible URI-Modul gibt an, dass der Body der HTTP-Anfrage als Rohdaten behandelt werden soll. Das bedeutet, dass der Body als String oder Text formatiert übertragen wird.
Wenn die Variable body_format nicht angegeben wird, verwendet das URI-Modul standardmäßig das JSON-Format. Es können jedoch auch andere Formate verwendet werden, wie zum Beispiel XML oder HTML. An dieser Stelle möchte ich noch kurz auf die Lookup-Funktion „{{ lookup(‚file‘, ‚/tmp/app_config.yml‘) }}“ eingehen. Damit wird die Datei als reiner Text im Body mitgeschickt.
- name: PUT
hosts: appserver
become: yes
vars:
api_endpoint: https://api.example.com/config
api_key: <your_api_key>
tasks:
- name: Download configuration file
get_url:
url: "{{ api_endpoint }}"
headers:
Authorization: "{{ api_key }}"
dest: /tmp/app_config.yml
- name: Change settings
# hier würde man beispielsweise eine Textdatei-Modul verwenden, um die Datei zu bearbeiten
- name: Upload configuration file
uri:
url: "{{ api_endpoint }}"
method: PUT
headers:
Authorization: "{{ api_key }}"
return_content: yes
body_format: raw
body: "{{ lookup('file', '/tmp/app_config.yml') }}"
register: api_response
- name: Show response from API
debug:
var: api_response.content
DELETE-Anfrage zum Löschen von Ressourcen:
Das letzte Beispiel dieser Reihe stellt ein Playbook zum Löschen aller vorhandenen Cloud-Ressourcen dar. Hierin müssen wir eine GET- und DELETE-Anfrage kombinieren. Des Weiteren zeigt das Playbook noch sehr schön, dass man das URI-Modul sehr gut meiner Schleife kombinieren kann. In so einem Fall werden mehrere DELETE-Anfragen an die API übermittelt.
- name: DELETE
hosts: localhost
connection: local
vars:
api_endpoint: https://api.example.com/resources
api_key: <your_api_key>
tasks:
- name: Show ressource ids
uri:
url: "{{ api_endpoint }}"
method: GET
headers:
Authorization: "{{ api_key }}"
return_content: yes
register: api_response
- name: Delete ressources
uri:
url: "{{ api_endpoint }}/{{ item }}"
method: DELETE
headers:
Authorization: "{{ api_key }}"
loop: "{{ api_response.json()['ids'] }}"
Welche Fallstricke sollte man bei API-Calls mit Ansible vermeiden?
Die obigen Beispiele wirken sehr einfach und eigentlich kann da doch gar nichts falsch laufen. Dem ist aber in der Praxis nicht so. Es gibt nämlich einige Stolpersteine. Welche das sind und worauf man im Allgemeinen achten sollte, möchte ich zum Abschluss dieses Artikels vorstellen.
- Authentifizierung: Stelle sicher, dass du über die notwendigen Zugangsdaten verfügst, um auf die API zugreifen zu können, und dass diese sicher gespeichert werden. Achte darauf, dass die Zugangsdaten nicht ungeschützt in den Playbooks oder in der Inventory-Datei gespeichert werden.
- Validierung von Eingaben: Überprüfe, ob alle erforderlichen Parameter und Daten in der Anfrage vorhanden sind und ob sie in der richtigen Form und im richtigen Format vorliegen, um eine erfolgreiche Anfrage zu gewährleisten.
- Fehlerbehandlung: Plane ein, wie im Fehlerfall vorgegangen werden soll. Das können zum Beispiel unerwartete HTTP-Statuscodes, Zeitüberschreitungen, fehlende Zugangsdaten oder fehlerhafte Anfragen sein. Es ist außerdem wichtig, sicherzustellen, dass das Playbook bei Fehlern nicht einfach abbricht, sondern auf mögliche Probleme reagiert.
- Sicherheit: Verwende stets eine sichere Verbindung (HTTPS) und stelle sicher, dass die API-Endpunkte, die du ansprichst, sicher und vertrauenswürdig sind. Vermeide es zudem auch, Zugangsdaten oder andere vertrauliche Informationen in Klartext in der URL oder im Body der Anfrage zu übertragen.
- Testen: Überprüfe deine Anfragen mehrmals in einer Testumgebung, bevor du sie in einer produktiven Umgebung einsetzt. Teste, ob die API-Aufrufe die erwarteten Ergebnisse liefern und ob die Anforderungen der API erfüllt werden.
Durch das Berücksichtigen dieser Fallstricke kannst du sicherstellen, dass deine API-Aufrufe mit Ansible reibungslos und sicher verlaufen. Natürlich ist nach dem Lesen dieses Artikels noch kein Meister vom Himmel gefallen. Studiere daher die Dokumentation der jeweiligen API genau. Nicht selten sind hier gute Code-Beispiele vorhanden, an denen du dich für deine Playbooks orientieren kannst.