Ansible-Tutorial: Setup eines Root-Servers mit Ansible

Hallo! Da dies mein erster Artikel auf HappyCoders.eu ist, will ich mich kurz vorstellen: Ich heiße Sven Woltmann, programmiere seit 35 Jahren (seit 20 beruflich) und mein Schwerpunkt ist Backend- und Microservice-Entwicklung mit Java und dem Spring-Framework. Ich habe mehrere Startups gegründet und arbeite seit neun Jahren als CTO bei der Fonpit AG, der Betreiberin der weltweit größten mehrsprachigen Android-Community AndroidPIT. Mehr zu mir und weitere Schwerpunkte erfährt ihr, wenn ihr auf meinen Namen klickt.

Bevor ich über meine Schwerpunktthemen schreibe, zeige ich euch in dieser ersten Artikelserie, 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 findet ihr 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

Die Serie werde ich in folgende vier Teile aufteilen:

  1. Setup eines Root-Servers mit Ansible
  2. Setup von User-Accounts, SSH und Firewall mit Ansible
  3. Setup von Docker, MySQL und WordPress mit Ansible
  4. Setup von HAProxy und eines HTTPS-Zertifikats von Let’s Encrypt mit Ansible

Ich plane – sobald der Blog läuft – eine zweite Tutorial-Serie zu schreiben über die Entwicklung von Microservices mit Java, Spring Boot, Spring Data und Logging via ELK-Stack, welche dann mittels Jenkins gebaut und in einem Kubernetes-Cluster deployed werden, wobei ich den Jenkins-Job als Code schreiben und Kubernetes selbstverständlich wieder via Ansible installieren werde.

Noch eines vorab: Vielleicht fragt ihr euch, warum ich Windows benutze. Die Antwort ist: Ich bin mit Windows aufgewachsen. Mac finde ich auch schick, aber für das gleiche Geld bekomme ich einen deutlich leistungsfähigeren Windows-PC/-Laptop. Und Linux benutze ich auf dem Server sehr gerne, doch die Desktop Environments gefallen mir persönlich nicht. Und seit es das Linux-Subsystem für Windows gibt, entfällt ja auch der Umweg über das Aufsetzen einer VM, um bspw. Ansible nutzen zu können.

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:

ssh root@happy1.happycoders.eu
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

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 euch, wie ihr unter Windows 10 ein vollwertiges Linux laufen lassen könnt – und zwar parallel zu Windows – und das ganz ohne den Overhead einer VM. Je nach gewählter Linux-Distribution folgt ihr 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

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

happy1.happycoders.eu

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

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

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, seht ihr 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 euch 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)

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 euch 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

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, seht ihr 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

Diesen Schritt erkläre ich euch 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 seht ihr, 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"

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

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

Die Werte in der Task-Definition ersetze ich nun durch die entsprechenden Variablennamen, die ich – wie ihr es schon bei inventory_hostname gesehen habt – 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

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

Ü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

Weitere Infos dazu findet ihr 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

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

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

ssh root@happy1.happycoders.eu
cat /etc/debian_version
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

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)

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

Ich führe das Playbook wie folgt aus:

ansible-playbook --extra-vars "target=happy1.happycoders.eu" install_python.yml

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 euch 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 euch im zweiten Teil der Serie: Setup von User-Accounts, SSH und Firewall mit Ansible.

Ich hoffe euch hat dieser erste Teil der Serie gefallen und bedanke mich bei euch, dass ihr bis zum Ende dabei geblieben seid. Ich würde mich freuen, wenn ihr einen Kommentar hinterlasst und mir mitteilt, wie euch das Tutorial gefallen habt, was ich hätte besser machen können und auch, was für Themen euch generell interessieren. Vielen Dank!

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.