- Ansible für automatisierte Roll-Outs – der Einstieg
- Ansible für automatisierte Roll-Outs – das erste Playbook
Nachdem wir uns im ersten Teil der Serie häuslich eingerichtet haben, werden wir jetzt wirklich ans Eingemachte gehen und das Basisplaybook, sowie die ersten Aktionen bauen.
Begrifflichkeiten
Ansible nennt den kompletten Ablauf, den man startet, ein „Playbook“. Ein Playbook wiederum enthält sogenannte Plays, die dann wiederum die Tasks enthalten. Diese ganzen Dinge werden in Files festgehalten, die im YAML Format sind. YAML ist ein einfaches Format, um strukturierte Informationen in Textdateien zu speichern.
Fangen wir mal hinten an: Tasks sind konkrete Aufgaben, die auf einem Host durchgeführt werden sollen, wie zum Beispiel das Anlegen eines Benutzers, das Erstellen eines Config Files mit einem bestimmten Inhalt oder das Starten eines Dienstes. Hier ist ein Beispiel für einen Task, der eine Config Datei aus einem Template erzeugt und diese auf dem Zielhost unter /etc/ntp.conf
ablegt:
1 2 3 4 5 6 7 8 |
- name: Deploy /etc/ntp.conf template: backup: true src: ntp.conf.j2 dest: '/etc/ntp.conf' owner: root group: root mode: 0644 |
Die Einrückung definiert in YAML die Blöcke (wie in Python).
Ein Play fasst nun mehrere Tasks zusammen und kombiniert diese mit einer Gruppe von Hosts, auf denen diese ausgeführt werden. Durch diese Strukturierung ist es möglich Multi-Host Deployments zu bauen, bei denen auf verschiedenen Hosts auch verschiedene Aufgaben ausgeführt werden müssen (LAMP Plattform: Apache Server, Datenbank Server, evtl. Loadbalancer, …).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
--- - hosts: webservers tasks: - name: Ensure apache is installed package: name=apache2 state=present - name: write the apache config file template: src=httpd.j2 dest=/etc/apache2/apache2.conf notify: - restart apache - name: ensure apache is running (and enable it at boot) service: name=apache2 state=started enabled=yes handlers: - name: restart apache service: name=httpd state=restarted |
Dieses Play installiert das Paket apache2
auf allen Hosts aus der Gruppe webservers
, schreibt eine Config in das Config Verzeichnis und stellt sicher, dass Apache beim Systemstart automatisch hochgefahren wird und auch gerade läuft. Der Handler ist eine weitere Spezialität, der man oft begegnet: falls eine neue Config Datei geschrieben wurde (und nur dann), wird der Apache Daemon von diesem Handler neu gestartet.
Hier sehen wir auch gleich eine Eigenschaft von Ansible: Tasks werden nur ausgeführt, wenn diese auch eine Änderung bewirken. Falls also in dem zweiten Task keine Änderung am Config File stattfindet, wird auch der Apache nicht neu gestartet. Diese Eigenschaft wird als „Idempotenz“ bezeichnet. Man kann also (im Allgemeinen) ein Playbook auch mehrfach ausführen und das Endergebnis ist immer dasselbe.
Ein Playbook fasst jetzt einfach mehrere Plays zusammen, sodass zum Beispiel auf der Gruppe webservers
Apache eingerichtet wird und auf der Gruppe dbservers
MariaDB.
Oft wird man die einzelnen Plays in externe Dateien auslagern, um die Lesbarkeit zu verbessern und nicht alles in einer großen Datei zu haben. Hierzu existiert ein entsprechender include
Task:
1 2 3 |
- hosts: webservers tasks: include: webserver_stuff.yml |
Also los…
Für den Anfang halten wir unser Playbook einfach, verzichten auf Gruppen und machen mit dem einfachen inventory File aus dem letzten Teil weiter:
1 2 3 4 |
control01 portal01 compute01 compute02 |
Unsere erste Aufgabe soll sein, dass alle Hosts eine Login Meldung ausgeben (aus /etc/motd
) und ein richtiger Admin User angelegt wird, der sich per sudo zu root machen kann. Zur besseren Erweiterbarkeit werden wir von vornherein die Tasks auf mehrere Files aufteilen. Hierzu legen wir in unserem Playbook Verzeichnis erstmal ein weiteres Unterverzeichnis namens includes
an: cd MeinPlaybook; mkdir includes
.
Jetzt bearbeiten wir unser site.yml
Playbook und fügen dort die entsprechenden Tasks hinzu. Hierzu müssen wir unter hosts:
eine Gruppe angeben. Da wir keine Gruppen definiert haben, verwenden wir hier einfach die immer verfügbare Gruppe all
, welche alle im Inventory vorkommenden Hosts umfasst. (Anmerkung: jedes YAML File beginnt mit ---
– einfach mit reinschreiben)
1 2 3 4 5 6 7 |
--- - hosts: all tasks: - name: Include motd tasks include: includes/motd.yml - name: Include Create Admin User tasks include: includes/admin.yml |
motd.yml
Um die Login Nachricht auf meinen Systemen etwas zurechtzustutzen (ich verwende Ubuntu 16.04LTS), müssen einige Dateien aus /etc/update-motd.d
entfernt werden und das File /etc/motd
muss mit entsprechendem Inhalt versehen werden.
Zuerst räumen wir mal die Einzelfiles aus /etc/update-motd.d
weg. Dazu benutzen wir das Ansible file
Modul, mit dem man das Vorhandensein und das Fehlen von Dateien und Verzeichnissen steuern kann.
Ubuntu gibt standardmäßig beim Login eine Reihe von Links aus, die auf die Hilfe verweisen und bei Cloud-Instanzen (AWS, OpenStack usw.) wird auch noch ein Hinweis auf cloud-init ausgegeben. Beides benötige ich nicht bei jedem Login und möchte es daher entfernen. Die Meldungen kommen aus /etc/update-motd.d/10-help-text
bzw. 51-cloudguest
. Diese beiden Dateien müssen also gelöscht werden, was wir mit folgendem Task erledigen (den wir einfach in includes/motd.yml
schreiben):
1 2 3 4 5 6 7 8 |
--- - name: Remove unneccessary files from motd message file: path: "/etc/update-motd.d/{{ item }} state: absent with_items: - 10-help-text - 51-cloudguest |
In der ersten Zeile geben wir einen Namen für den Task an. Dieser wird beim Ausführen des Playbooks angezeigt und erleichtert das verfolgen der durchgeführten Tasks und das Troubleshooting, wenn mal was nicht funktioniert. Die nächste Zeile gibt das zu verwendende Ansible Modul an, in diesem Fall file
. Im Anschluss folgen die Parameter für das File Modul:
path
spezifiziert den (absoluten!) Pfad zu der Datei, um die es gehtstate
spezifiziert den Zielzustand, in unserem Fall alsoabsent
Bei der Angabe des Pfades sehen wir auch zum ersten Mal eine Variable in Jinja2 Template Notation: {{ item }}
. Diese Variable entsteht aus dem nächsten Block: with_items
spezifiziert eine Liste, über die dann der Task in einer Schleife laufen soll, damit wir nicht zwei fast identische Tasks für zwei Dateien benötigen. Die Variable {{ item }}
wird bei jedem Durchlauf nacheinander mit den Werte aus der Liste belegt.
admin.yml
Als zweiten Job wollen wir einen Admin User anlegen, der sich mittels eines SSH Keys einloggen kann und der sudo
ohne Passwort ausführen kann. Dazu legen wir einen normalen User an, installieren den entsprechenden SSH Key in der Datei ~/.ssh/authorized_keys
. Ferner fügen wir den Benutzer der Gruppe sudonopw
hinzu (diese legen wir vorher an) und erlauben dieser Gruppe dann den Befehl sudo
ohne Passwort zu verwenden.
Diesen Code fügen wir in die Datei admin.yml
ein. Zuerst stellen wir sicher, dass die Gruppe sudonopw
existiert:
1 2 3 4 5 6 |
--- - name: Create sudonopw group group: name: sudonopw state: present system: true |
Dieser Task legt bei Bedarf die Gruppe an (state: present
) und markiert diese als Systemgruppe. Dies hat unter Linux im Allgemeinen keine besondere Bedeutung, aber je nach Distribution werden Gruppen in einer bestimmten GID Range als Systemgruppen bezeichnet. Der entsprechende Parameter sorgt dafür, dass diese Regelungen eingehalten werden.
Als nächstes legen wir einen Benutzer an, und stellen sicher, dass dieser in der gewünschten Gruppe ist, wozu wir das user
Modul verwenden:
1 2 3 4 5 6 7 8 9 10 |
- name: Create admin user user: append: True groups: - sudonopw name: admuser comment: "Admin User" password: "!!" state: present update_password: always |
Die name
und state
Parameter funktionieren wie gehabt und comment
hinterlegt einfach nur einen Kommentar zu dem User in /etc/passwd
. Der append
Parameter gibt an, ob die die unter groups
spezifizierten Gruppen zu schon bestehenden hinzugefügt werden sollen (append: True
) oder ob diese die schon bestehenden komplett ersetzen sollen (append: False
). Mittels password
geben wir einen Passwort Hash an. Diesen muss man bei Bedarf selbst erstellen – wir setzen hier den Hash auf den (unmöglichen) Wert „!!“, was dazu führt, dass dieser User sich nicht mit einem Passwort anmelden kann, sondern nur mittels eines SSH Keys, den wir im nächsten Task installieren:
1 2 3 4 5 6 7 |
- name: Install SSH key for admin user authorized_key: user: admuser manage_dir: True exclusive: True state: present key: "{{ lookup('file', 'files/admin_ssh_key.pub') }}" |
Das authorized_key
Modul von Ansible ermöglicht die Verwaltung von SSH Keys für Benutzer. Mittels user
geben wir den Benutzer an, dessen Keys wir verwalten wollen, manage_dir
teilt dem Modul mit, dass es das ~/.ssh
Verzeichnis anlegen soll, falls es noch nicht existiert und exclusive
sorgt dafür, dass nur genau der eine unter key
angegebene Key für diesen User vorhanden ist (eventuelle andere Keys, die bereits vorhanden sind, werden entfernt!).
Beim key
Parameter sehen wir auch gleich eine weitere Funktion: da SSH Keys inline im Playbook sehr unhandlich sein können, da diese unter Umständen sehr lang sind, lesen wir den Key mittels der lookup
Funktion aus einer Datei ein, welche wir unter files/admin_ssh_key.pub
im Playbookverzeichnis ablegen.
Zuletzt müssen wir noch die Konfiguration für passwortloses sudo erledigen. Hierzu verwenden wir das template
Modul, mit dem man eine Datei aus einer Vorlage erstellen kann. An dieser Stelle sind zwar noch keine Variablen erforderlich, so dass wir auch einfach die fertige Datei auf das Zielsystem kopieren könnten, aber im weiteren Verlauf werden wir die Funktionalität noch erweitern, so dass es Sinn macht, das Modul hier schon zu verwenden.
1 2 3 4 5 6 7 |
- name: Deploy sudo config for group sudonopw template: dest: /etc/sudoers.d/sudonopw owner: root group: root mode: 0440 src: sudonopw.j2 |
owner
, group
und mode
geben die UNIX Berechtigungen der Zieldatei an, dest
zeigt an, wo die Datei auf dem Zielsystem liegen soll und src
spezifiziert das Templatefile, das als Quelle genutzt werden soll. Das template
Modul sucht Templates per Default unter templates/
im Playbook Verzeichnis (unter anderem) und genau dort legen wir die entsprechende Datei jetzt ab. Die Endung „.j2
“ soll anzeigen, dass es sich bei der Datei um ein Jinja2 Template handelt und nicht etwa um eine direkt verwendbare Datei. Der Inhalt der Datei ist im Folgenden angegeben und entspricht der Standard sudoers
Syntax.:
1 2 3 |
# {{ ansible_managed }} Defaults env_keep+=SSH_AUTH_SOCK %sudonopw ALL = (root) NOPASSWD: ALL |
Die einzige Besonderheit ist die erste Zeile. Genau wie in den Playbooks können in einem Template Variablen referenziert werden. Das hier verwendete Makro ansible_managed
gibt einfach einen Text aus, der anzeigt, dass die Config Datei durch Ansible erzeugt und verwaltet wird und der Inhalt sich daher automatisch ändern kann.
Und ab dafür…
Unser erstes (noch recht einfaches) Playbook ist fertig und wir müssen es nur noch laufen lassen.
Das geschieht mit dem Befehl ansible-playbook
:
1 |
ansible-playbook -i hosts site.yml |
Mit -i
geben wir das Inventoryfile an und site.yml
ist der Name des Playbookfiles selbst. Wenn alles in Ordnung war, sollten jetzt einige „changed“ Tasks vorliegen und die Änderungen an den Zielsystemen wurden durchgeführt.
Ausblick
Im nächsten Teil schauen wir uns dann an, wie man wiederkehrende Aufgaben in eigene Module (die so genannten „Rollen“) auslagert und wie man diese mit Parametern versehen kann.