Ansible Best Practices: Do’s and Don’ts

Ansible ist großartig, um damit seine bestehende Infrastruktur zu automatisieren. Sei es Systeme zu patchen, Konfigurationen an die eigenen Bedürfnisse anzupassen oder Software damit auszurollen. Der Einstieg ist nicht wahnsinnig komplex und erste Erfolge sind schnell erreicht. Allerdings lohnt es sich von Anfang an, ein paar grundlegende Punkte zu beachten, damit man sich auch für die Zukunft nichts verbaut und eine flexible und robuste Automatisierung hat. Natürlich kann man hier auch schnell vom Hundertsten ins Tausendste kommen, daher ist der folgende Artikel ein erster Überblick mit den aus meiner Sicht wichtigsten Punkten, die beim Einsatz von Ansible zu berücksichtigen sind. Aufgeteilt ist das Ganze in „Do’s“ und „Don’ts“.

Do’s

Verwende FQCN (Fully Qualified Collection Names)

Ansible hat viele Collections, die unterschiedliche Module für die Automatisierung anbieten. Neben den Standard Collections von RedHat gibt es auch einige von Amazon für AWS, für Grafana, Proxmox usw. Einen Überblick liefert folgende Seite.

Da es durchaus vorkommen kann, dass Module den gleichen Namen in unterschiedlichen Collections haben, ist es wichtig, immer den sogenannten FQCN zu verwenden.

Gut:

- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present

Schlecht:

- name: Install nginx
apt:
name: nginx
state: present

Variablen mit klaren Namen verwenden

Versuche so viel wie möglich mit Variablen zu arbeiten. Statische Werte mögen auf den ersten Blick gut und einfach funktionieren, machen dir aber bei nachträglichen Änderungen das Leben eventuell sehr schwer. Auch bei unterschiedlichen Umgebungen sind Variablen extrem wichtig, da hier unterschiedliche Werte die absolute Regel pro Umgebung sind. Wichtig ist es auch, die Variablen sinnvoll zu benennen, damit auch dritte Personen leicht erkennen können, was mit der Variable gemeint ist. Wenn du z.B. den Webserver „nginx“ installierst und eine Konfiguration für Port 443 anpassen willst, sollte die Variable für den Port nicht einfach nur „port“ heißen, sondern z.B. „nginx_https_port“.

Halte Variablen dort, wo sie hingehören

Variablen können in Ansible an mehreren Stellen definiert und verwendet werden. Das mag am Anfang etwas verwirrend sein, bietet aber extrem viele Möglichkeiten für eine sehr dynamische und flexible Automatisierung.

Für eine Ansible Rolle kommen alle Standardwerte, damit eine Rolle funktioniert, in die Datei „defaults/main.yml“. Diese Werte können auch einfach durch höher priorisierte Variablen überschrieben werden.

Sollte es Variablen erfordern, die nicht so ohne Weiteres durch höher priorisierte Variablen überschrieben werden dürfen, können diese innerhalb der Rolle in „vars/main.yml“ definiert werden. Dazu bedarf es dann Logik innerhalb der Playbooks, um diese Werte zu manipulieren bzw. müssen die neuen Werte beim Aufruf des Playbooks als „extra vars“ mit angegeben werden.

Um die bereits definierten Variablen in der „defaults/main.yml“ zu überschreiben, können diese im Inventory in den „host_vars“ für einen spezifischen Host oder in den „group_vars“ für eine bestimmte Gruppe von Hosts definiert werden. Da diese Variablen eine höhere Priorität haben, werden diese zur Laufzeit auch angewendet.

Schreibe idempotente Tasks

Ein sehr wichtiger Teil von Ansible ist die Idempotenz. Damit ist gemeint, dass das Ausführen eines Playbooks mehrmals hintereinander immer zum gleichen, korrekten Zustand führt, ohne dabei unbeabsichtigte Änderungen oder Fehler zu verursachen. Ansible gewährleistet dies, indem es vor jeder Aktion den aktuellen Zustand des Zielsystems überprüft und nur dann Änderungen vornimmt, wenn eine Abweichung vom gewünschten Zielzustand festgestellt wird. Das ist allerdings nicht mit jedem einzelnen Modul direkt möglich. Wenn als Beispiel ein Befehl mit „ansible.builtin.command“ oder „ansible.builtin.shell“ ausgeführt wird, kann Ansible nicht wissen, was das Ergebnis sein soll. Diese Tasks sind somit nicht idempotent. Das lässt sich aber durch eigens definierte Abfragen dennoch realisieren. Allerdings ist der Aufwand dafür natürlich deutlich höher, manchmal ist es aber nicht anders realisierbar.

Wenn ich wie im Beispiel vorhin nginx installieren möchte, könnte ich das über einen Befehl realisieren.

- name: Install nginx via shell
ansible.builtin.shell: apt-get install -y nginx

Diesen Befehl würde Ansible jedes Mal ausführen. Besser ist in diesem Fall ein passendes Modul zu verwenden.

- name: Ensure nginx is installed
ansible.builtin.apt:
name: nginx
state: present

Verwende Rollen statt riesiger Playbooks

Um eine gute Lesbarkeit, klare Struktur und bessere Wiederverwendbarkeit zu gewährleisten, sollten lieber Rollen als große komplexe Playbooks verwendet werden. In Rollen lassen sich natürlich auch sehr komplexe Szenarien abbilden, allerdings wesentlich besser strukturiert. So lassen sich auch komplexe Aktionen in unterschiedliche Dateien aufteilen, die dann gezielt in der richtigen Reihenfolge aufgerufen werden.

Eine Rolle kann wie folgt mit einer recht einfachen Struktur aufgebaut werden. In den Ordner Tasks kommen alle Dateien mit Aktionen, die ausgeführt werden sollen. Die Handlers sind eine Ansammlung von Aktionen die regelmäßig ausgeführt werden sollen, aber nicht redundant in den Tasks definiert werden sollen. Das kann z.B. das Neustarten von Systemd Daemons sein. In den Ordner Templates kommen Template Dateien, die dynamisch zur Laufzeit auf dem Ziel angepasst werden sollen. Ein Beispiel hierfür sind die Konfigurationsdateien für den nginx Webserver. Die Standardvariablen wie Ports und Pfade werden im Ordner defaults definiert.

roles/
nginx/
tasks/
handlers/
templates/
defaults/
vars/
files/

Verwende Tags

Tags ermöglichen es, spezifische Tasks oder ganze Rollen mit Labels zu versehen und später nur diese selektiv auszuführen. Das spart Zeit und Ressourcen, da nicht jedes Mal das gesamte Playbook durchlaufen muss. Beispielsweise können nur die Konfigurationstasks ausgeführt werden, ohne die Installation zu wiederholen. Bei der Ausführung wird dann die Kommandozeilen-Option „--tags“ oder „–skip-tags“ genutzt, um festzulegen, welche Tasks laufen sollen oder welche übersprungen werden. Das ist besonders nützlich in komplexen Infrastruktur-Szenarien, wo verschiedene Teams unterschiedliche Aspekte der Bereitstellung handhaben: Ein Team kann beispielsweise nur mit dem Tag „security“ arbeiten, um Sicherheits-Updates einzuspielen, während ein anderes Team mit dem Tag „monitoring“ nur die Überwachung konfiguriert. Tags verbessern also die Flexibilität, Wartbarkeit und Effizienz deiner Ansible-Automatisierung erheblich.

Linting & Formatierung

Die Verwendung von yamllint und ansible-lint sind Best Practices zur Sicherung der Qualität in Ansible Projekten. Yamllint überprüft die YAML Syntax und die Formatierung von Ansible Dateien auf Fehler und Stilverstöße, wodurch Probleme bereits vor der Ausführung erkannt werden. Dies verhindert verwirrende Laufzeitfehler, die durch fehlerhafte Einrückung, ungültige Zeichen oder falsche Struktur entstehen können. Ansible-lint geht noch einen Schritt weiter und analysiert die Ansible spezifische Semantik. Es prüft auf Anti Patterns, sicherheitstechnische Schwachstellen, veraltete Module und Best Practice Verstöße.

Don´ts

Shell und Command zweckentfremden

Die Verlockung, die Module „ansible.builtin.command“ und „ansible.builtin.shell“ zu nutzen ist groß. In vielen Szenarien ist es auch nicht anders lösbar. Um die Idempotenz zu gewährleisten sollte aber immer versucht werden, ein passendes Modul zu verwenden.

Schlecht:

ansible.builtin.shell: systemctl restart nginx

Gut:

ansible.builtin.service:
name: nginx
state: restarted

Harte Codierung von Werten

Auch wenn es „nur ein schneller Test“ ist, versuche immer mit Variablen zu arbeiten. Ein einmal gesetzter statischer Wert, wird vermutlich nachträglich nicht mehr geändert. Erst wenn das Playbook Ärger macht, weil ein anderes Ergebnis für einen anderen Wert erwartet wird. Dabei wird der Aufwand deutlich größer.

Schlecht:

- name: Create user
ansible.builtin.user:
name: admin

Gut:

- name: Create user
ansible.builtin.user:
name: "{{ app_user }}"

Tasks ohne Namen

Definiere für jeden Task einen aussagekräftigen Namen. Dies dient für die bessere Lesbarkeit des Codes und hilft auch beim Troubleshooting, da die Namen des aktuell ausgeführten Task auch in den Logs erscheinen.

Zu große Playbooks

Vermeide es, zu viele Aktionen in eine einzelne yml Datei zu packen. Definiere lieber eine ordentliche Ansible Rolle und strukturiere die Aktionen in einzelne Dateien. Diese können dann per Includes in der richtigen Reihenfolge ausgeführt werden. Das macht es auch wieder einfacher die Rolle zu lesen und zu verstehen.

Ignorieren von Fehlern

Ansible hat mit der Anweisung „ignore_errors“ eine „ganz Tolle“ Funktion, um im Falle eines fehlgeschlagenen Task weitere Aktionen auszuführen, statt das Playbook mit einem Fehler zu beenden. Das sollte natürlich im Optimalfall niemals verwendet werden, da es in den meisten Fällen einfach nur auf ein schlecht geschriebenes Playbook zurückzuführen ist. Viel wichtiger ist es, ein ordentliches Error Handling zu verwenden. Sollte in Ausnahmefällen kein Weg an „ignore_errors“ vorbeiführen, kann es mit Bedacht eingesetzt werden.

Unklare Projektstruktur

Eine gute Struktur in Ansible muss nicht komplex sein, auch wenn die auszuführenden Aktionen und auch die Umgebungen durchaus komplex sein können. Im folgenden Bild ist ein Beispiel für eine gängige Ansible Struktur abgebildet.

Ansible Projekt Struktur

Fazit

Gute Ansible Praktiken drehen sich nicht nur um Funktionalität, sondern um:

  • Wartbarkeit
  • Lesbarkeit
  • Vorhersagbarkeit

Sauber strukturierter Code spart langfristig Zeit, reduziert Fehler und macht Automatisierung skalierbar.