Was der Serverbetrieb von der Anwendungsentwicklung lernen kann
Die Automatisierung hat längst den Alltag der IT erobert. Produkte können unbeaufsichtigt installiert werden, die Serverüberwachung erhält Fähigkeiten zur Selbstheilung und Cloud-Dienste werden per API ferngesteuert. Trotzdem beschränkt sich die Automatisierung vielerorts auf monotone und fehleranfällige Arbeitsabläufe. Im Serverbetrieb muss ein deutliches Umdenken erfolgen – weg vom Skript hin zur Beschreibung des Zielzustands.
Konzepte der Anwendungsentwicklung
In der Anwendungsentwicklung ist seit über zehn Jahren etabliert, dass in kleinen Inkrementen entwickelt, getestet und ausgerollt wird. Der Quelltext wird dabei in einem Versionskontrollsystem gespeichert, um Veränderungen nachvollziehbar zu machen. Mit jeder Veränderung des Quelltextes werden automatisierte Tests ausgeführt, um die Korrektheit zu prüfen. Dies nennt sich Continuous Integration. Wenn nach der erfolgreichen Überprüfung ein automatisches Deployment in einer Test-Umgebung durchgeführt wird, spricht man von Continuous Deployment. In einer solchen Testumgebung werden weiterführende Tests, z. B. manuelle Prüfschritte durch den Menschen ermöglicht.
Die Königsklasse stellt das Continuous Delivery dar. Dabei werden dem Kunden erfolgreich getestete Features automatisch zur Verfügung gestellt. Oft wird ein Produkt nicht nach jeder kleinen Änderung ausgeliefert. Stattdessen werden mehrere Features gesammelt und in einer gemeinsamen Aktualisierung bereitgestellt.
Continuous Integration ist besonders effizient, wenn die Veränderungen am Quelltext nur einen kleinen Umfang haben und die automatisierten Tests schnell ausgeführt werden können. Dann ist ein zeitnahes Feedback an den Entwickler möglich, ob die Quelltextänderung erfolgreich war oder Nacharbeiten benötigt werden. Damit diese Arbeitsweise funktioniert, müssen Entwicklungsziele zerlegt werden, so dass beherrschbare Aufgabenstellungen übrigbleiben. Diese Aufgaben sollten innerhalb weniger Stunden implementiert werden können.
Der Entwicklungsprozess wird so um eine schnelle Rückkopplung zur Qualität des Quelltextes erweitert (s. Abb. 1). Der Entwickler übernimmt dadurch nicht nur die Verantwortung für die Umsetzung einer Aufgabe, sondern auch für die Korrektheit der Implementierung.
Schlüsseltechnologien
Die Übertragung agiler Entwicklungsprinzipien auf den Serverbetrieb gelingt besonders einfach durch die Beschreibung des Zielzustands anstatt der Automatisierung des Weges. Allerdings wird im Serverbetrieb oft die imperative Automatisierung eingesetzt. Dabei werden die einzelnen Schritte bis zur Erreichung des Zielzustands explizit beschrieben. Diese Vorgehensweise entwickelt sich ganz natürlich durch die Abarbeitung eines Installationshandbuchs. Jeder Schritt dieses Handbuchs wird nacheinander durch einen dedizierten Abschnitt im Skript behandelt. Die Überprüfung der Funktionalität des daraus entstehenden Skripts erfolgt in der Regel aber nur oberflächlich (Smoke Test).
Die imperative Automatisierung leidet allerdings unter dem Nachteil, dass ein Fehler im Ablauf einen unklaren Zustand zur Folge hat. In der Regel scheitert dadurch der Neustart desselben Skripts. Manche Veränderungen am Zielsystem können sogar nur einmalig vorgenommen werden oder haben einen neuen Ausgangszustand zur Folge. Damit ein Skript reproduzierbar dasselbe Ergebnis erreicht, müssen zusätzliche Kontrollstrukturen eingebaut werden. Damit geht oft einher, dass Parameter eingeführt werden, um das Skript zu verallgemeinern. Ab diesem Punkt werden die Vorteile des Skripts besonders deutlich, da über die Automatisierung hinausgegangen wurde und das Verhalten standardisiert wurde. Dadurch können dieselben und ähnliche Anforderungen mit demselben Skript realisiert und wiederholt werden. Infolgedessen erhöht sich die Zuverlässigkeit der automatisierten Komponenten, da dasselbe Skript immer wieder eingesetzt und dadurch kontinuierlich getestet wird.
Der Standard zur Verwaltung von Containern ist heute der Werkzeugkasten von Docker.
Der professionelle Einsatz der Automatisierung wird früher oder später zur Folge haben, dass die Parameter in eine Konfigurationsdatei ausgelagert werden, um unterschiedliche Einsatzzwecke mit demselben Skript zu verarbeiten. Dadurch gelangt man zur deklarativen Automatisierung. Anstatt zu beschreiben, wie ein Ziel erreicht wird, konzentriert man sich darauf, wie das Ziel aussieht. Die folgende Tabelle zeigt den Unterschied in der Herangehensweise. Während in der imperativen Automatisierung die einzelnen Schritte beschrieben werden müssen, kann in der deklarativen Automatisierung der Zielzustand beschrieben werden:
Imperativ | Deklarativ |
---|---|
apt-get update apt-get install -y nginx service nginx start |
service: ..name: nginx ..state: started |
Kommt die deklarative Automatisierung flächendeckend zum Einsatz, spricht man von Configuration Management, da nicht mehr die Automatisierung im Vordergrund steht, sondern die Verwaltung des Zielzustands der betroffenen Serversysteme. Etablierte Werkzeuge für das Configuration Management sind Chef, Puppet und Ansible. Sie liefern umfangreiche Bibliotheken, mit denen die meisten Aspekte eines Systems deklarativ automatisiert werden können, ohne den Code selbst zu schreiben oder zu pflegen.
Containerisierung
Nachdem die Containerisierung in den letzten Jahren das Linux-Umfeld erobert hat, investiert Microsoft auch stark in die Umsetzung auf Windows-Clients und -Servern. Der De-Facto-Standard zur Verwaltung von Containern ist heute der Werkzeugkasten des US-amerikanischen Unternehmens Docker, das aktuell seinen fünften Geburtstag feiert. Im Vergleich zur Hardware-Virtualisierung stellt die Containerisierung eine leichtgewichtige Abstraktionsschicht dar. Container werden in derselben Instanz eines Betriebssystems ausgeführt. Tatsächlich handelt es sich lediglich um Prozesse, die mittels spezieller Funktionen im Kernel voneinander isoliert ausgeführt werden.
Zwar existieren die Konzepte zur Isolation von Prozessen im Linux-Kernel schon seit vielen Jahren, aber erst Docker verhalf 2013 der Technologie zum Durchbruch. Die Besonderheit besteht darin, dass Docker die Verwaltung von Containern vereinfacht, indem die Erstellung und das Deployment von Containern auf einfache Weise automatisiert wird. Dadurch führen die Docker-Werkzeuge ein Configuration Management für Container ein.
Die Isolation von Prozessen mithilfe von Containern hat im Serverbetrieb einen entscheidenden Vorteil: Der Ressourcen-Overhead pro Dienst wird effektiv reduziert. Wohingegen bei virtuellen Maschinen Dienste jeweils eine dedizierte Instanz des Betriebssystems erhalten, können mehrere containerisierte Dienste in einer virtuellen Maschine ausgeführt werden. Dadurch werden die Ressourcen effizienter genutzt. Das folgende Beispiel demonstriert die Isolation eines Prozesses mithilfe eines Containers:
$ docker container run -d --name test nginx:alpine $ ps faux PID TTY STAT TIME COMMAND [...] 14657 ? Ss 0:00 nginx: master process nginx -g daemon off; 14689 ? S 0:00 nginx: worker process [...] $ docker container exec test ps PID USER TIME COMMAND 1 root 0:00 nginx: master process nginx -g daemon off; 8 nginx 0:00 nginx: worker process 18 root 0:00 ps
Beim Vergleich der Prozesslisten fällt auf, dass aus der Sicht des Betriebssystems (erste Liste) zusätzliche Prozesse gestartet wurden. Innerhalb des Containers (zweite Liste) scheinen allerdings keine Prozesse neben denen des Webserver nginx zu existieren. Außerdem fällt auf, dass innerhalb des Containers die Vergabe der Prozess-ID wieder bei 1 startet. Dadurch erscheint die Laufzeitumgebung wie ein unberührtes System.
Durch die Ausführung mehrerer Dienste pro Host tritt die Verwaltung der Ressourcennutzung in den Vordergrund. Wie schon für virtuelle Maschinen muss das Verhalten und die dadurch generierte Last der Dienste bekannt sein. Nur so kann deren Auswirkung auf den Host und damit die Auswirkung auf andere darauf ausgeführte Dienste in Betracht gezogen werden. Die gegenseitige Beeinträchtigung kann dadurch reduziert werden, dass die Ressourcennutzung eingeschränkt wird. Das folgende Beispiel demonstriert die Reservierung von 1 GB Arbeitsspeicher sowie die Limitierung auf 2 GB Arbeitsspeicher:
$ docker container run -d \ --memory 2048m \ --memory-reservation 1024m \ nginx:alpine
Offensichtlich wurde in den vorhergehenden Beispielen ein Paket verwendet, in dem bereits Nginx installiert ist. Darin zeigt sich ein weiterer Vorteil von Docker. Ein so genanntes Docker-Image enthält die vollständige Laufzeitumgebung eines Prozesses, so dass die Ausführung ohne Abhängigkeiten zum darunter liegenden Betriebssystem gelingt. Dadurch wird das Verhalten des Prozesses auf anderen Maschinen reproduzierbar. Für die Erstellung eines solchen Pakets hat Docker eine sehr einfache Skriptsprache eingeführt:
$ ls Dockerfile $ cat Dockerfile FROM alpine RUN apk update \ && apk add nginx CMD nginx -g „daemon off;” $ docker image build --tag mynginx .
Dem aufmerksamen Leser ist vermutlich aufgefallen, dass die Befehle im Dockerfile gegen ein Basisimage (FROM) ausgeführt werden. Diese Basisimages werden im Docker Hub bereitgestellt und umfassen unterschiedliche Distributionen (Ubuntu, CentOS, Fedora etc.) und Dienste (Nginx, PostgreSQL, WordPress u. v. m.). Um diese Basisimages hat sich eine rege Community gebildet, in der Images erstellt und geteilt werden. Für den Unternehmenseinsatz kann eine solche Tauschplattform namens Registry auch selbst betrieben werden, um Eigenentwicklungen zu schützen.
Deployment-Prozess
Neben den technischen Vorteilen der Containerisierung verändert deren Einführung die Deployment-Prozesse (s. Abb. 3). In der klassischen Automatisierung des Serverbetriebs werden Installationsskripte im Rahmen des Deployments aufgerufen. Dabei müssen allerdings diverse Werkzeuge und Dienste installiert werden, was den Deployment-Prozess in die Länge zieht. Der Einsatz von Docker hingegen verlagert diese Schritte in die Entwicklungsphase. Da Werkzeuge und Dienste ein Teil des Docker-Image sind, werden diese bereits im Rahmen der Erstellung installiert.
Der entscheidende Vorteil besteht darin, dass mit Abschluss der Entwicklung das Docker-Image wie ein Artefakt in der Anwendungsentwicklung angesehen wird. Ein Artefakt ist das Endprodukt der Entwicklung und wird durch Tests geprüft und zum Kunden ausgerollt. Die Entdeckung eines Fehlers hat zur Folge, dass im Entwicklungsschritt der Fehler korrigiert und eine neues Artefakt erstellt werden muss.
Deployment von Serveranwendungen
Serveranwendungen bestehen selten aus einem isolierten Dienst. In der Regel setzen sie sich aus mehreren Diensten zusammen, die voneinander abhängig sind. Beispielsweise benötigt eine Webseite ein Datenbankbackend. Dann spricht man von mehrschichtigen Anwendungen bzw. Multi-Tier Applications.
Diese zeichnen sich außerdem dadurch aus, dass die Abhängigkeiten in der Konfiguration der Dienste ausgedrückt werden müssen. Das heißt, der Webserver muss wissen, wo sich das Datenbankbackend befindet. Dafür wurde von Docker das Werkzeug docker-compose ins Leben gerufen. Es verarbeitet eine deklarative Konfiguration im YAML-Format, die üblicherweise in einer Datei namens docker-compose.yml abgelegt wird. Darin können mehrere Dienste als Einheit definiert und konfiguriert werden:
version: ‘2.0‘ services: web: image: wordpress:4.9.3 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_PASSWORD: my5ecur3p4ssw0rd restart: always ports: 8080:80 db: image: mysql environment: MYSQL_ROOT_PASSWORD: my5ecur3p4ssw0rd restart: always
Unterhalb des Knotens services werden zwei Dienste namens web und db deklariert. Für beide wird mit dem Knoten image festgelegt, auf welchem Docker-Image die Dienste basieren. Die Images werden beim Deployment automatisch vom Docker Hub heruntergeladen. Die Konfiguration der containerisierten Dienste wird üblicherweise über Umgebungsvariablen unterhalb des Knotens environment vorgenommen. In diesem Beispiel wird die Datenbank MySQL mit einem Kennwort konfiguriert, das WordPress ebenfalls bekannt gemacht wird. Die Option restart: always definiert, dass beide Dienste neugestartet werden sollen, falls ein unvorgesehenes Ereignis den Dienst stoppt. Da Container einen dedizierten Netzwerkadapter mit einer lokalen IP-Adresse erhalten, ermöglicht die Option ports das Veröffentlichen des Container-Ports 80 über den Host-Port 8080.
Der Zugriff von WordPress auf MySQL erfolgt innerhalb eines separaten Netzwerks, das durch docker-compose automatisch angelegt wird. Die Namen der deklarierten Dienste werden darin als DNS-Namen eingerichtet, so dass der Zugriff von WordPress auf MySQL leicht konfiguriert werden kann.
Der Aufruf von docker-compose erwartet die Datei docker-compose.yml im aktuellen Verzeichnis und verarbeitet die darin enthaltene Konfiguration. Mit dem folgenden Befehl werden alle Dienste gestartet und deren Log-Ausgaben auf der Konsole angezeigt:
$ docker-compose up
Für produktive Dienste bietet sich an, die Container mit einem zusätzlichen Parameter im Hintergrund zu starten:
$ docker-compose up -d
CI/CD im Serverbetrieb
Setzt man die zuvor beschriebenen Konzepte zusammen, erhält man zum Einen reproduzierbare Laufzeitumgebungen mithilfe des Dockerfile. Zum Anderen können Dienste mithilfe von docker-compose.yml beschrieben werden. Für die Umsetzung sind die Werkzeuge von Docker verantwortlich. Dieses Konzept nennt sich Infrastructure-as-Code und bezeichnet die Automatisierung von Infrastrukturkomponenten durch deklarative Konfigurationsdateien.
Eine Besonderheit von docker-compose ist die Fähigkeit, Veränderungen an der Zielkonfiguration ebenso ausrollen zu können. Wenn sich an der Konfiguration des zuvor beschriebenen WordPress-Dienstes etwas verändert, hat der nächste Deployment-Prozess zur Folge, dass die Container mit der veralteten Konfiguration durch aktualisierte Versionen ersetzt werden. Dabei werden die überholten Container allerdings nicht verändert, sondern gestoppt. Anschließend werden neue Container mit der korrigierten Konfiguration gestartet. Dadurch werden inkrementelle Veränderungen vermieden, die ansonsten zu einem schwer nachvollziehbaren Zustand führen. Zusätzlich können die alten Container im gestoppten Zustand aufgehoben werden, bis die korrekte Funktion der neuen Container geprüft werden konnte.
Eine typische Aktualisierung ist beispielsweise das Ausrollen einer neuen Version von WordPress (4.9.4 anstatt 4.9.3), um Sicherheitslücken zu stopfen oder neue Features zu erhalten. Dann muss die Dienstbeschreibung angepasst werden:
version: ‘2.0‘ services: web: image: wordpress:4.9.4 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_PASSWORD: my5ecur3p4ssw0rd restart: always ports: 8080:80 db: image: mysql environment: MYSQL_ROOT_PASSWORD: my5ecur3p4ssw0rd restart: always
Anschließend kann derselbe Befehl wie beim initialen Deployment verwendet werden, um die Änderungen auszurollen:
docker-compose up -d
Durch die im Container paketierte Laufzeitumgebung können Dienste auch sehr leicht skaliert werden. Durch zusätzliche Container wird zum Einen die Ausfallsicherheit und zum Anderen die Performance erhöht. Der folgende Befehl erhöht die Anzahl der WordPress-Container im Deployment auf zwei – aber Achtung, die Container enthalten unterschiedliche Versionen von wp-content:
docker-compose scale web=2
Auf einige Konzepte und Anforderungen wurde bisher noch nicht eingegangen. Diese können im Serverbetrieb allerdings nicht außer Acht gelassen werden. Da jeder Dienst ausfallen kann und bestimmt mal ausfallen wird, müssen mehrere Hosts zur Ausführung von Containern bereitgestellt werden. Außerdem bedarf es einer Kontrollinstanz zur Überwachung der Umgebung. Werden Unregelmäßigkeiten im Betrieb festgestellt, d. h. wird von der bekannten Zielkonfiguration abgewichen, findet eine automatische Korrektur statt. Wird ein Container ungeplant beendet, wird dieser neugestartet. Geht er verloren, wird er anhand der bekannten Konfiguration neu erzeugt. Die Kontrollinstanz wird in der Containerisierung auch Orchestrierung oder Orchestrierer genannt. Bekannte Beispiele dafür sind Docker Swarm, Kubernetes sowie Cattle von Rancher Labs.
Zusätzlich müssen Aspekte wie die Verwaltung von schützenswerten Daten (z. B. Kennwörter), persistenter Speicher, Monitoring und Zertifikate bedacht werden. In der folgenden Abbildung werden die notwendigen Komponenten einer Containerumgebung dargestellt.
Die grünen Komponenten müssen manuell bereitgestellt werden. Die blauen Komponenten gehören zur Orchestrierung. Die grauen Komponenten werden durch die Orchestrierung automatisch verwaltet, wenn die orangenen Komponenten ausgerollt oder aktualisiert werden.
Übung macht den Meister
Die Automatisierung ist aus dem Serverbetrieb schon seit Jahren nicht mehr wegzudenken. Ein kontinuierlicher Verbesserungsprozess gelingt allerdings nur mit den etablierten Konzepten aus der Anwendungsentwicklung. Durch konsequentes Überprüfen aller Schritte werden viele Fehler frühzeitig aufgedeckt (Fail Early) sowie deren Wiederholung vermieden.
Dafür ist allerdings ein Umdenken notwendig. Anstatt von imperativen Skripten müssen deklarative Konfigurationen zum Einsatz kommen, mit denen der Zielzustand eines Systems beschrieben wird. Viele Verfechter dieser Arbeitsweise streben sogar Everything-as-Code an. Dadurch sollen alle Aspekte einer IT-Infrastruktur maschinell verarbeitet und konfiguriert werden. Das umfasst dann beispielsweise auch den Build-, Test- und Deployment-Prozess selbst.
Für die Umstellung empfiehlt es sich, einen weniger kritischen Dienst in Gänze deklarativ zu beschreiben und auszurollen. Dafür ist zwar Disziplin notwendig, aber nach einer Umgewöhnungsphase werden die Vorteile des hohen Automatisierungsgrades spürbar. Die Containerisierung ermöglicht die Überprüfung der entwickelten Dienste und ihrer Laufzeitumgebung unter reproduzierbaren Bedingungen.
Ein wichtiger Aspekt dieser neuen Arbeitsweise ist die ständige Prüfung der etablierten Prozesse. Nur wenn die Wiederholbarkeit sichergestellt wird, kann einem Zwischenfall entspannt entgegengeblickt werden. Dann steht die Korrektur des Fehlers im Vordergrund, ohne an der Funktionalität des automatisierten Deployment-Prozesses zweifeln zu müssen.
Übung macht halt immer noch den Meister!