Ansible-Tutorial: Setup eines Root-Servers mit Ansible - Feature-Bild

Ansible-Tutorial: Setup eines Root-Servers mit Ansible

Autor-Bild
von Sven Woltmann – 3. Oktober 2018

Artikelserie: Ansible-Tutorial

Teil 1: Setup eines Root-Servers mit Ansible

Teil 2: User-Accounts, SSH und Firewall

Teil 3: Docker, MySQL und WordPress

Teil 4: HAProxy + HTTPS von Let’s Encrypt

(Melde dich für den HappyCoders-Newsletter an, um sofort über neue Teile informiert zu werden.)

In dieser Artikelserie zeige ich dir, wie ich den Server, auf dem mein Blog laufen wird, einrichte – beginnend mit der Installation des Betriebssystem-Images über die SSH- und Firewall-Konfiguration über die Installation von Docker, MySQL und WordPress bis hin zum Setup von HAProxy und der Einrichtung eines kostenlosen HTTPS-Zertifikats von Let’s Encrypt via Certbot.

Das ganze werde ich komplett mit Ansible machen – also Infrastruktur as Code – schließlich heißt der Blog ja „Happy Coders“! Den vollständigen Code findest du in meinem GitLab-Repository: https://gitlab.com/SvenWoltmann/happycoders-tutorial-server-setup.

Zusätzlich zum Artikel gibt es hier ein Video-Tutorial: Setup eines Root-Servers mit Ansible

Los geht’s mit dem ersten Teil, dem Aufsetzen des Servers.

Mein Server

Ich verwende einen dedizierten Server mit Xeon E3-CPU der dritten Generation, 32 GB RAM und zwei Enterprise HDDs aus der Serverbörse von Hetzner für 26 Euro im Monat. Eine VM bei Amazon mit vier virtuellen Cores, 32 GB RAM und 2 TB HDD Speicher geht bei 250 Euro los, beim Deutschen Anbieter ProftBricks liegt man bei vier Intel-Cores, 32 GB RAM und 2 TB HDDs bei 298,88 Euro. Genau vergleichen lässt sich die CPU-Leistung ohne Tests nicht, doch dieser schnelle Check zeigt, dass ich in jedem Fall für den dedizierten Server nur einen Bruchteil dessen zahle, was eine VM kosten würde. Bei beiden Cloud-Anbietern kommen übrigens noch Traffic-Kosten hinzu, während bei Hetzner unbegrenzter Traffic mit 1 GBit/s Brandbreite im Preis enthalten ist.

Ich beginne mit der Installation des Betriebssystem-Images. Der Hetzner-Server befindet sich aktuell im Rescue-System und zur Authentifizierung habe ich bei der Bestellung meinen Public Key hinterlegt, so dass ich mich nun – nachdem ich die IP-Adresse in meine lokale hosts-Datei eingetragen habe – mit ssh einloggen kann:

Code-Sprache: Klartext (plaintext)
Hetzner Rescue-System
Hetzner Rescue-System

Manuell würde man nun mit installimage die Installation starten. Das Skript hat allerdings auch einen automatischen Modus – die Parameter dafür erfährt man, indem man installimage -h eingibt:

Hetzner installimage parameters
Hetzner installimage parameters

Das Kommando selbst liegt unter root/.oldroot/nfs/install, die verfügbaren Images liegen unter /root/.oldroot/nfs/images/, von dort wähle ich das aktuellste Debian-Image Debian-95-stretch-64-minimal.tar.gz. Ich möchte ein RAID 1 anlegen, die ersten 512 MB zur ext3-Boot-Partition machen und den Rest als ext4-Dateisystem anlegen. Das Kommando würde nun wie folgt aussehen:

root/.oldroot/nfs/install/installimage \ -a \ -n happy1.happycoders.eu \ -b grub \ -r yes \ -l 1 \ -i /root/.oldroot/nfs/images/Debian-95-stretch-64-minimal.tar.gz \ -p /boot:ext3:512M,/:ext4:all \ -d sda,sdb
Code-Sprache: Klartext (plaintext)

Das möchte ich nun allerdings nicht manuell ausführen, sondern automatisiert über Ansible von meinem Rechner aus, so dass ich es jederzeit, mit einem Klick, auch auf anderen Server ausführen kann.

Was ist Ansible?

Ansible ist ein Open-Source-Tool zur Automatisierung der Konfiguration und Administration der IT-Infrastruktur. Im Gegensatz zu anderen Tools wie Chef oder Puppet benötigt Ansible weder eine Server-Komponente noch ein aufwendig zu wartendes oder kostenpflichtiges Repository, aus dem der Server sich die Konfigurationdaten zieht. Ansible braucht lediglich eine SSH-Verbindung zum Server.

Ansible führt sogenannte „Tasks“ aus, wobei jeder Task auf ein „Modul“ verweist, welches eine bestimmte Aufgabe ausführt, wie z. B. ein Package installieren, eine Datei kopieren oder einen User anlegen. Man definiert dabei nicht, was genau das Modul tun soll, sondern was der gewünschte Zielzustand ist. Das Modul entscheidet dann, was es tun muss, um dem gewünschten Zielzustand zu erreichen.

Immer dann, wenn ich ein Ansible-Modul zum ersten Mal verwende, werde ich es kurz erklären und auf entsprechende Dokumentation auf der Ansible-Webseite verlinken.

Installation von Ansible

Die Installation wird ausführlich im Ansible Installation Guide beschrieben.

Windows wird nicht direkt unterstützt, doch das ist kein Problem. Der Windows Subsystem for Linux Installation Guide zeigt dir, wie du unter Windows 10 ein vollwertiges Linux laufen lassen kannst – und zwar parallel zu Windows – und das ganz ohne den Overhead einer VM. Je nach gewählter Linux-Distribution folgst du im Ansible Installation Guide dem entsprechenden Abschnitt. Da ich persönlich auf meinen Servern Debian verwende, habe ich dies auch auf meinem Windows-Laptop installiert:

Ansible unter Windows 10
Ansible unter Windows 10

Installation des Images mit Ansible

Zunächst erstelle ich ein neues Ansible-Projekt. Dies ist zunächst einmal nichts anderes als ein leeres Verzeichnis innerhalb meines Git-Monorepos: /happycoders/git/main/ansible/.

Als erstes benötigt Ansible eine hosts-Datei, in der die Server aufgelistet sind, die angesteuert werden sollen. In meinem Fall ist das lediglich ein Server: happy1.happycoders.eu. In der hosts-Datei können auch Hosts zu Gruppen zusammengefasst werden, um später mehrere Server gleichzeitig ansteuern zu können. Beispielsweise könnte ich noch einen zweiten Server happy2.happycoders.eu eintragen und beide Server zu einer Gruppe „production“ zusammenfassen oder Testserver unter einer Gruppe „test“ hinzufügen. Das würde dann so aussehen:

[production] happy1.happycoders.eu happy2.happycoders.eu [test] test1.happycoders.eu test2.happycoders.eu
Code-Sprache: Zugriffs-Log (accesslog)

Da ich momentan nur einen Server habe, sieht meine hosts-Datei erst einmal so aus:

happy1.happycoders.eu
Code-Sprache: Klartext (plaintext)

Ansible erwartet diese Datei standardmäßig in /etc/ansible. Um dies zu ändern, muss im Projektverzeichnis noch eine Datei ansible.cfg angelegt und darin der Pfad der hosts-Datei angeben werden. Die ansible.cfg sieht wie folgt aus:

[defaults] inventory = hosts
Code-Sprache: Zugriffs-Log (accesslog)

Ansible-Playbook erstellen

Ansible-Aktionen („Tasks“) werden in sogenannten „Rollen“ implementiert. Welche dieser Rollen auf welchen Servern ausgeführt werden sollen, definiert man in „Playbooks“. Ich beginne mit dem Playbook: Dazu erstelle ich im Projektverzeichnis /happycoders/git/main/ansible/ eine Playbook-Datei install_image.yml, welche folgenden Inhalt hat:

--- - hosts: "{{ target }}" gather_facts: false remote_user: root roles: - install_image
Code-Sprache: YAML (yaml)

Erklärung der Parameter:

  • Mit dem Parameter hosts wird angegeben, auf welchem Server das Playbook ausgeführt werden soll. Da ich dieses Playbook auch für alle zukünftigen Server ausführen können möchte, verwende ich hierfür eine Variable target. Diese übergebe ich beim Ausführen des ansible-playbook-Kommandos auf der Kommandozeile (wie genau, siehst du in Kürze).
  • Über gather_facts: false sage ich Ansible, dass es keine Daten über den Server sammeln soll. Das könnte es im Moment auch nicht, denn dazu müsste auf dem Server Python installiert sein.
  • Mit remote_user gebe ich an, dass Ansible sich als root-User zum Server verbinden soll. Einen anderen User gibt es zu diesem Zeitpunkt noch nicht.
  • Und schließlich definiere ich über das Array roles, welche Rollen auf dem Server ausgeführt werden sollen. Die Rolle hat in diesem Fall denselben Namen wie das Playbook.

Ansible-Rolle erstellen

Im zweiten Schritt lege ich die gleichnamige Rolle an. Da Rollen aus mehr als einer Datei bestehen können, liegt jede Rolle in einem Verzeichnis roles/<Rollenname>/. Ich erstelle also unter meinem Projektverzeichnis das Verzeichnis roles/install_image/.

Eine Rolle kann neben den zuvor erwähnten Tasks auch Dateien, Templates und Variablen enthalten, welche jeweils in einem eigenen Unterverzeichnis liegen. Doch das zeige ich dir später – für die install_image-Rolle brauche ich lediglich Tasks und für die lege ich das gleichnamige Verzeichnis roles/install_image/tasks/ an. In diesem Verzeichnis erwartet Ansible die eigentlichen Task-Definitionen in der Datei main.yml. Darin definiere ich die folgenden fünf Tasks, welche von Ansible in genau dieser Reihenfolge ausgeführt werden:

  1. Im ersten Schritt muss ich Python installieren, um die folgenden Schritte überhaupt ausführen zu können.
  2. Danach prüfe ich, ob das installimage-Skript, mit dem ich das Betriebssystem-Image installieren möchte, an der erwarteten Stelle existiert.
  3. Sollte das nicht der Fall sein, soll Ansible das Playbook abbrechen.
  4. Anderfalls führe ich nun das installimage-Skript aus.
  5. Abschließend soll der Server rebootet werden.

In Ansible-Code sieht das wie folgt aus:

Schritt 1 – Python installieren:

- name: Install Python on the rescue image raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal)
Code-Sprache: YAML (yaml)

In der ersten Zeile definiere ich den Namen des Tasks. Dieser wird beim Ausführen der Rolle angezeigt. In der zweiten Zeile sage ich, dass das Ansible-Modul raw ausgeführt werden soll, welches das angegebene Kommando auf dem Server ausführt. Es gibt eigentlich eine komfortablere Möglichkeit ein Package zu installieren, doch diese steht erst dann zur Verfügung, wenn Python installiert ist. Diese Methode werde ich dir später zeigen.

Schritt 2 – Prüfen, ob das Skript existiert:

- name: Check if "installimage" script exists stat: path: /root/.oldroot/nfs/install/installimage register: stat_result
Code-Sprache: YAML (yaml)

Prüfen, ob eine Datei existiert, kann man mit dem Ansible-Modul stat: über den Parameter path wird festgelegt, welche Datei geprüft werden soll und mit register definiert man den Namen einer Variablen, in der das Ergebnis der Prüfung gespeichert wird. Wie dieses Ergebnis ausgewertet wird, siehst du im nächsten Schritt.

Schritt 3 – Abbrechen, wenn das Skript nicht existiert:

- block: - name: "Abort when \"installimage\" script doesn't exist" debug: msg: "installimage script not found; you have either already installed the image or Hetzner renamed the script. Aborting." - meta: end_play when: stat_result.stat.exists == false
Code-Sprache: YAML (yaml)

Diesen Schritt erkläre ich dir von unten nach oben: Mit der Ansible-Conditional when lege ich fest, dass ich nur in einem bestimmten Fall etwas ausführen möchte, nämlich dann, wenn die Datei nicht existiert (stat_result.stat.exists == false). Ist das der Fall, möchte ich zwei Aktionen ausführen: über das Ansible-Modul debug eine Fehlermeldung anzeigen und über das Ansible-Module meta das Playbook abbrechen. Um dies zu ermöglichen, fasse ich diese beiden Aktionen in einem "Block" zusammen. Bei der zweiten Aktion siehst du, dass ein Tasks nicht immer einen Namen haben muss. Ich könnte hier auch einen Namen definieren, z. B. mit name: End the Playbook, aber das halte ich hier für überflüssig.

Schritt 4 – Skript ausführen:

- name: Execute installimage shell: "/root/.oldroot/nfs/install/installimage -a -n {{ inventory_hostname }} -b grub -r yes -l 1 -i /root/.oldroot/nfs/images/Debian-94-stretch-64-minimal.tar.gz -p /boot:ext3:512M,/:ext4:all -d sda,sdb -f yes"
Code-Sprache: YAML (yaml)

Das Ansible-Modul shell ermöglicht es mir das installimage-Skript auszuführen. Da ich hier die vom Ansible-Playbook definierte Variable inventory_hostname (Hostname des Servers) ersetzen lassen möchte, muss ich diese in doppelte geschweifte Klammern einschließen und das komplette Kommando in Anführungszeichen setzen.

Schritt 5 – Reboot:

- name: Rebooting… shell: reboot
Code-Sprache: YAML (yaml)

Dieser Schritt erklärt sich von selbst.

Variablen extrahieren

Was mir an dem Skript nicht gefällt ist, dass ziemlich viel hardcodiert ist. Vielleicht ändert Hetzner in Zukunft den Pfad des installimage-Skripts; vielleicht möchte ich in Zukunft ein anderes Image installieren oder ein RAID 5 auf mehr als zwei Festplatten anlegen.

Um das zu ermöglichen, extrahiere ich Werte, die ich evtl. in Zukunft ändern möchte, in eine sogenannte Host-Variablendatei. Diese muss im Verzeichnis host_vars liegen und bekommt den Namen des Servers. Ich lege also die Datei host_vars/happy1.happycoders.eu an und beschreibe diese wie folgt (der Kommentar in der Zeile „raid ...“ ist nicht Teil des Variablen-Werts):

--- install_image: path: /root/.oldroot/nfs/install/installimage image: /root/.oldroot/nfs/images/Debian-95-stretch-64-minimal.tar.gz raid: yes -l 1 # yes, level: 1 drives: sda,sdb
Code-Sprache: YAML (yaml)

Die Werte in der Task-Definition ersetze ich nun durch die entsprechenden Variablennamen, die ich – wie du es schon bei inventory_hostname gesehen hast – in doppelte geschweifte Klammern einschließe. Die Datei roles/install_image/tasks/main.yml ist nun vollständig und sieht wie folgt aus:

--- - name: Install Python on the rescue image raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal) - name: Check if "installimage" script exists stat: path: "{{ install_image.path }}" register: stat_result - block: - name: "Abort when \"installimage\" script doesn't exist" debug: msg: "installimage script not found; you have either already installed the image or Hetzner renamed the script. Aborting." - meta: end_play when: stat_result.stat.exists == false - name: Execute installimage shell: "{{ install_image.path }} -a -n {{ inventory_hostname }} -b grub -r {{ install_image.raid }} -i {{ install_image.image }} -p /boot:ext3:512M,/:ext4:all -d {{ install_image.drives }} -f yes" - name: Rebooting… shell: reboot
Code-Sprache: Stylus (stylus)

Playbook ausführen

Ich bin nun bereit das Playbook auszuführen. Dazu wechsle ich wieder in das Projektverzeichnis und führe dort folgendes Kommando aus:

ansible-playbook --extra-vars "target=happy1.happycoders.eu" install_image.yml
Code-Sprache: Klartext (plaintext)

Über den Parameter --extra-vars übergebe ich dem Playbook die Variable target, die ich in der ersten Zeile des Playbooks verwendet habe.

Achtung: Im Linux Subsystem für Windows 10 sind die Laufwerke standardmäßig so gemounted, dass jeder User volle Schreib- und Leserechte auf alle Verzeichnisse und Dateien hat. Dies führt dazu, dass Ansible zunächst mit folgender Warnung abbricht:

Warnung bzgl. der Schreibrechte beim Ausführen von Ansible unter Windows
Warnung bzgl. der Schreibrechte beim Ausführen von Ansible unter Windows

Um das zu ändern, muss das Laufwerk, welches das Ansible-Verzeichnis enthält, zunächst geunmounted und dann mit aktivierter Option metadata wieder gemounted werden. Handelt es sich beispielsweise um das C:-Laufwerk, würde man das wie folgt machen:

umount /mnt/c mount -t drvfs C: /mnt/c -o noatime,uid=1000,gid=1000,metadata
Code-Sprache: Klartext (plaintext)

Weitere Infos dazu findest du im Microsoft-Blog-Artikel Chmod/Chown WSL Improvements. Als nächstes kann man dem Projektverzeichnis dann beispielsweise wie folgt die globalen Schreibrechte entziehen:

chmod 755 /happycoders/git/main/ansible
Code-Sprache: Klartext (plaintext)

Zurück zum ansible-playbook-Kommando. Wenn alles gut läuft, sieht man nun folgende Ausgaben auf der Konsole:

Ausführung des install_image-Playbooks
Ausführung des install_image-Playbooks

Während des Reboots bricht die Verbindung ab, das ist völlig OK. Nach kurzer Zeit sollte der Server wieder erreichbar sein. Da sich durch das neue Image der Fingerprint des Server-Keys geändert hat, muss ich den alten Key erstmal aus meiner known_hosts-Datei entfernen:

ssh-keygen -f "/home/sven/.ssh/known_hosts" -R happy1.happycoders.eu ssh-keygen -f "/home/sven/.ssh/known_hosts" -R 46.4.99.9
Code-Sprache: Klartext (plaintext)

Danach kann ich mich erneut auf dem Server einloggen, und stelle fest, dass das neue Image erfolgreich installiert wurde:

ssh [email protected] cat /etc/debian_version
Code-Sprache: Klartext (plaintext)
Neues Image erfolgreich installiert
Neues Image erfolgreich installiert

Noch einmal Python

Das installierte Debian-Image hat standardmäßig Python nicht installiert.

In meinem Playbook für die weitere Konfiguration des Servers werde ich nicht auf die „Facts“ verzichten können, die Ansible beim Start eines Playbooks vom Server sammelt (IP-Adresse, Betriebssystem-Version, etc.). Dafür muss allerdings Python installiert sein.

Zum Sammeln der „Facts“ muss gather_facts auf true gesetzen werden – für die Installation von Python hingegen auf false. Folglich muss die Installation von Python in ein separates Playbook ausgelagert werden, welches vor allen kommenden Playbooks ausgeführt werden muss.

Dafür erstelle ich in meinem Projektverzeichnis die Playbook-Datei install_python.yml mit folgendem Inhalt:

--- - hosts: "{{ target }}" gather_facts: false remote_user: root roles: - install_python
Code-Sprache: YAML (yaml)

Der Inhalt ist fast der gleiche wie im install_image.yml-Playbook mit dem einzigen Unterschied, dass hier die Rolle install_python referenziert wird. Zur Erklärung des Playbooks verweise ich zurück auf die Beschreibung des install_image.yml-Playbooks.

Für die neue Rolle lege ich die Datei roles/install_python/tasks/main.yml an:

--- - name: Install Python raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal)
Code-Sprache: YAML (yaml)

Dies gleicht dem ersten Task der install_image-Rolle. Da wir Softwareentwickler ungern duplizierten Code sehen, kann ich nun den ersten Task aus der install_image-Rolle entfernen und stattdessen im install_image.yml-Playbook die install_python-Rolle vor die install_image-Rolle setzen:

roles: - install_python - install_image
Code-Sprache: YAML (yaml)

Ich führe das Playbook wie folgt aus:

ansible-playbook --extra-vars "target=happy1.happycoders.eu" install_python.yml
Code-Sprache: Klartext (plaintext)

Und bekomme folgendes ausgegeben:

Installation von Python mit Ansible
Installation von Python mit Ansible

Zusammenfassung und Ausblick

In diesem Artikel habe ich erklärt, was Ansible ist und dir gezeigt, wie ich Ansible dazu nutze, automatisiert das Betriebssystem-Image auf einem Root-Server zu installieren.

Als nächstes möchte ich alle verfügbaren Patches einspielen, den Hostnamen festlegen und eine Hosts-Datei anlegen, einen weiteren User anlegen, um mich nicht als root einloggen zu müssen, die SSH-Konfiguration optimieren, Bash-Aliase anlegen, einige hilfreiche Kommandozeilen-Tools installieren und die Firewall konfigurieren.

Wie das funktioniert, zeige ich dir im zweiten Teil der Serie: Setup von User-Accounts, SSH und Firewall mit Ansible.

Wenn dir dieser erste Teil der Serie gefallen hat, hinterlasse mir gerne einen Kommentar oder teile den Artikel über einen der Share-Buttons am Ende.

Möchtest du informiert werden, wenn neue Artikel auf HappyCoders.eu veröffentlicht werden? Dann klick hier, um dich für den HappyCoders.eu Newsletter anzumelden.