Ein typischer erster Arbeitstag in einem neuen Projekt: Es handelt sich um ein ganz „gewöhnliches“ Java-EE-Projekt. Ich bekomme Zugangsdaten für eine Versionsverwaltung und ein Wiki-System. Ich habe Glück, der Code lässt sich sofort bauen. Nun würde ich meine nagelneuen Artefakte gerne deployen und in Aktion sehen. Das bedeutet zunächst: Wiki-Seiten wälzen. Dort finde ich Verweise auf den verwendeten Application-Server und Fragmente für die Konfiguration. Ich wühle mich also durch die Downloadseiten des Herstellers, um die korrekte Version zu finden, und editiere dann die Konfigurationsdateien. Für die verwendete Datenbank bekomme ich vom Kollegen ein Image für eine Virtualisierungssoftware. Allerdings, so warnt er mich, müsse ich einige Schemaänderungen noch per Hand nachpflegen. Etwas später habe ich die Migrationsskripte gefunden und führe sie aus. Per Hand deploye ich die neuen Artefakte und es entstehen merkwürdige Fehler. Die Fehlersuche mit dem Kollegen offenbart, dass ich noch einige obligatorische Verzeichnisse anlegen muss und die Konfigurationsdateien aus dem Wiki nicht ohne Änderungen zu der Datenbank im Image passen. Schlussendlich bin ich in der Lage, das Projekt zum Laufen zu bringen. Aber ob mein System wohl identisch mit dem des Kollegen ist? Oder der Testumgebung? Oder der Produktion? Und mit wie viel Aufwand für den Aufbau einer weiteren Staging- Umgebung muss man rechnen?
Infrastructure as Code
Kommt Ihnen das geschilderte Szenario bekannt vor? Wie aber lässt sich die Situation verbessern? Wir können uns einiges bei uns selbst abschauen: Der Quelltext des Projekts ließ sich ohne Probleme auschecken und erfolgreich bauen. Das hat vor allem zwei Gründe: Erstens verwalten wir Quelltext in einem Versionskontrollsystem (VCS) und Zweitens wird er regelmäßig automatisch gebaut und getestet. Wir können Änderungen vornehmen, ohne befürchten zu müssen, sie nicht mehr rückgängig machen zu können. Das VCS verrät uns bei richtiger Anwendung, wann, von wem und warum eine Änderung gemacht wurde; und jeder kann jederzeit den aktuellen Stand abrufen. Darüber hinaus ist durch die automatisierten Tests eine gewisse Qualität gesichert. Wenn es uns nun gelänge, alle Schritte zum Aufsetzen der Infrastruktur, hier also der Datenbank, des Applikationsservers und das Deployen der Anwendung mit einer ausführbaren Dokumentation zu versehen, könnte sie ebenfalls im VCS gespeichert und automatisch verifiziert werden.
Dieses Vorgehen wird als „Infrastructure as Code“ bezeichnet. Dabei geht es nicht vorrangig um das Einsetzen eines neuen Tools, sondern um einen Paradigmenwechsel. Das Einrichten von Systemen wird als Programmieren begriffen und wir übertragen Vorgehensweisen aus der klassischen Entwicklung auf das Entwickeln von Infrastruktur. Lauffähige Systeme werden damit vom statischen Artefakt, das einmal eingerichtet wird, zum Ergebnis eines ausführbaren Programms. Das macht sie duplizierbar und versionierbar.
Einmal geschriebener Code kann in einem virtuellen Rechner auf dem Entwicklercomputer, in der Cloud oder auf echter Hardware ausgeführt werden. Dadurch gelingt es, Unterschiede zwischen Entwickler-, Test- und Produktivsystemen zu verringern. Darüber hinaus wird das lauffähige System zum Wegwerfartikel: Es kann jederzeit gelöscht, neu erstellt oder dupliziert werden. Das erlaubt es zum Beispiel, Systeme nur für die Laufzeit bestimmter Tests zu erstellen, und macht damit einen Hauptvorteil des Cloud Computing, also die kurze Provisionierungsdauer und Abrechnung nach tatsächlich genutzter Zeit, überhaupt erst in größerem Umfang nutzbar. Infrastruktur zu programmieren und versionieren statt nur zu konfigurieren hat Vorteile weit über den geschilderten Anwendungsfall des Einrichtens der Entwicklungsumgebung hinaus.
Chef
Um Infrastruktur zu programmieren, bedarf es einer Programmiersprache, die darauf zugeschnitten ist. Chef stellt eine solche domänenspezifische Sprache (DSL) bereit. Als Framework leistet Chef aber noch mehr: Es sammelt alle Informationen an einer Stelle und bietet ein REST API, um verschiedenste Eigenschaften der Infrastruktur abzufragen. So können Fragen wie „Was ist die IP-Adresse des Webservers für die Umgebung DEV?“ oder „Auf welchem Rechner ist Version X des Programms Y installiert?“ beantwortet werden.
Große und kleine Köche
Chef gibt es in verschiedenen Ausführungen. Als Chef Solo läuft es auf einem Computer und führt Chef-Programme aus, die lokal ohne Verbindung vorliegen. Diese einfache Variante eignet sich vor allem für erste Experimente mit Chef. In der Praxis kommt meist Chef Client/Server zum Einsatz (Abb. 1). Dabei gibt es einen zentralen Server. Auf jedem Rechner, der über Chef konfiguriert werden soll, läuft der Chef-Client in der Regel als Service. Der Chef-Client fragt in konfigurierbaren, regelmäßigen Abständen über das REST API des Servers den gewünschten Zustand ab und konfiguriert das System gegebenenfalls um.
Entwickler oder Systemadministratoren nehmen Veränderungen vor, indem sie mit dem Programm Knife Befehle an das REST API des Chef-Servers senden. Neben Knife kann auch eine Weboberfläche verwendet werden, um Chef zu steuern. Langfristig ist man mit Knife aber flexibler und schneller.
Cookbooks
Chef-Programme werden in so genannten Cookbooks (Kochbücher) abgelegt. Ein
Cookbook aggregiert verschiedene Objekte, darunter die eigentlichen
Programme (so genannte Recipes), Attributdefinitionen und Template-Dateien.
All diese Dinge werden innerhalb einer vorgegebenen Verzeichnisstruktur
abgelegt, die mit dem Befehl knife cookbook create name
angelegt werden
kann. In der Regel werden die Cookbooks in einer Versionsverwaltung
gespeichert (bevorzugt Git, aber auch andere sind möglich) und von dort mit
knife cookbook upload name_des_cookbook
zum Chef-Server übertragen. Eine
Liste aller auf dem Chef-Server vorhandenen Cookbooks kann mit knife
cookbook list
abgerufen werden.
Rezepte, Ressourcen und Provider
Recipes sind in der Regel eine Aneinanderreihung von Ressourcen (Resource). Eine Resource ist eine plattformunabhängige Repräsentation von Dingen, die auf dem Zielrechner (dem Node) konfiguriert werden sollen. Resources selbst besitzen keine Ausführungslogik. Zu jeder Resource gibt es aber einen oder mehrere Provider, die für eine oder mehrere Plattformen die tatsächliche Ausführung übernehmen. Tatsächlich funktioniert das in der Regel nur für die gängigen Linux-Distributionen. Chef unter Windows sei vorerst nur den experimentierfreudigeren Menschen empfohlen.
Betrachten wir als Beispiel die Package Resource (Listing 1). Sie
kann genutzt werden, um benötigte Softwarepakete zu installieren. Da es aber
viele verschiedene Wege gibt, Pakete zu installieren, existieren auch
etliche verschiedene Provider für diese Resource: darunter für Apt, Rpm,
MacPorts oder Yum. Durch Angabe des Provider
-Attributs kann ein bestimmter
Provider gewählt werden, ansonsten wird abhängig vom Zielsystem ein
sinnvoller Default gewählt.
Ein weiteres Attribut, das jede Resource besitzt, ist action
. Es bestimmt,
welche Aktion ausgeführt werden soll. Die gültigen Werte sind abhängig von
der konkreten Resource; für die Package Resource sind es install
,
upgrade
, remove
und purge
. Die meisten verfügbaren Resources sind
idempotent. Das heißt, sie können mehrfach ausgeführt werden, ohne dass es
zu einem Fehler kommt. Zum Beispiel wird ein Provider für die Package
Resource beim Ausführen der Aktion install
zunächst prüfen, ob das
geforderte Paket schon installiert ist. Nur wenn das nicht der Fall ist,
wird eine Installation durchgeführt. Falls die Installation aus temporären
Gründen scheitert (weil z.B. ein Software-Repository nicht verfügbar ist),
wird sie beim nächsten Lauf des Chef-Clients erneut gestartet. Auf diese
Weise konvergiert die tatsächliche Konfiguration des Zielsystems gegen das
gewünschte Ergebnis. Einige Resources sind nicht von sich aus idempotent,
hier muss der Entwickler selbst sicherstellen, dass es zu keinem Fehler
kommt, wenn sie mehrfach ausgeführt werden.
Das gilt zum Beispiel für die Script Resource, mit der vorhandene Bash-, Perl-, Python-, Ruby- oder CSH-Skripte ausgeführt werden können.
Template Resource
Eine häufig wiederkehrende Aufgabe ist das Anlegen von
Konfigurationsdateien. Chef unterstützt das durch die Template Resource.
Dazu wird im Cookbook ein Template-File angelegt, auf das dann im Rezept
verwiesen wird. Die Template-Dateien werden dabei im .erb-Format abgelegt,
sozusagen in der Ruby-Version des JSP-Markups. Dabei kann in Tags der Form
<% some_Code %>
Ruby-Code eingebettet werden. Ein Platzhalter der
Form <%=expression %>
wird durch den Wert des Ausdrucks ersetzt.
Listings 2 und 3 zeigen, wie Platzhalter in einem Template verwendet werden
können und wie im Recipe Platzhalter an Werte gebunden werden (hier wird
@Name an „Welt“ gebunden).
Die Template Resource verfügt (ebenso wie einige verwandte Resources) über
einen praktischen Mechanismus, um Cookbooks für mehrere Plattformen fit zu
machen: Im template
-Ordner des Cookbooks kann eine Datei des gleichen
Namens in verschiedenen Unterordnern abgelegt werden. Chef wählt dann anhand
des Hostnamens oder des Betriebssystems die passende Version aus. Für unser
Beispiel könnte das folgendermaßen aussehen:
Knoten und Attribute
Bei den bisher vorgestellten Beispielen handelt es sich im Grunde um „dumme“
Skripte. Sie unterscheiden sich prinzipiell nicht von anderen Lösungen wie
Shell- oder Perl-Skripten. Sie können in der gezeigten Form auf einem
Rechner mit Chef Solo auch ähnlich verwendet werden. Seine eigentliche
Stärke entfaltet Chef aber beim Einsatz eines Chef-Servers. Der Chef-Server
fügt einige wichtige Konzepte hinzu. Seine wichtigste Fähigkeit ist,
Informationen über die verwalteten Server zu sammeln und zu speichern. Ein
Server wird in Chef durch einen Node repräsentiert. Wenn ein neuer Server
konfiguriert werden soll, muss er Chef als Node bekannt gemacht werden.
Welche Nodes es gibt, kann mit knife
abgefragt werden:
Mit knife
können wir auch noch mehr Informationen über einen speziellen
Knoten abrufen:
Bei den angezeigten Daten handelt es sich um eine sehr kleine Auswahl von
wichtigen Attributen des Node. Mit der Opption -l
können alle Attribute
angezeigt werden. Chef (genauer ein Tool namens Ohai, das auf dem entfernten
Rechner ausgeführt wird) sammelt von sich aus zahlreiche nützliche
Informationen und speichert sie als Attribute des Knotens. Darunter zum
Beispiel IP-Adresse, Hostname, Informationen über installierte
Programmiersprachen, die Prozessorarchitektur, die Hauptspeichergröße und
Netzwerkschnittstellen.
Im obigen Beispiel sehen wir, dass der Knoten außerdem über eine Run List
verfügt. Sie bestimmt, welche Recipes und Roles (Rollen) auf dem Knoten
ausgeführt werden sollen. Roles sind eine Abstraktion, mit der sich mehrere
Recipes (und Attributes) zusammenfassen lassen. So könnte eine Role LAMP
zum Beispiel aus den Recipes Apache, Mysql und PHP bestehen. Roles
vereinfachen die Verwaltung mehrerer gleichartiger Server. Der Run List
eines Knotens muss dann nur die Rolle LAMP zugewiesen werden, um alle drei
Rezepte darauf auszuführen. Mit knife
können Knoten Recipes oder Roles
zugewiesen oder entfernt werden. Zum Beispiel kann mit folgendem Code dem
Knoten testServer
das Recipe tomcat hinzugefügt werden:
Neben den automatischen Attributen können Knoten beliebige eigene Attribute hinzugefügt werden. Damit lassen sich Recipes parametrisieren. Es gibt vier verschiedene Ebenen von Attributen (in aufsteigender Reihenfolge):
- default
- normal
- override
- automatic
Automatic Attributes können vom Benutzer nicht verändert werden, es sind jene, die durch Ohai gesetzt werden. Alle anderen können durch Attribute- Dateien in Cookbooks, durch Roles oder direkt am Node gesetzt werden. Bei mehrfacher Setzung des gleichen Attributs gilt eine festgelegte Reihenfolge:
- default attributes in Attributdateien
- default attributes in Roles
- default attributes, die auf einem Node gesetzt sind
- normal attributes in Attributdateien
- normal attributes, die auf einem Node gesetzt sind
- override attributes in Attributdateien
- override attributes in Roles
- override attributes, die auf einem Node gesetzt sind
Üblicherweise setzen Cookbooks Default-Werte in ihren Attributdateien
(Listing 4). In Rollen oder auf konkreten Knoten werden sie dann
gegebenenfalls überschrieben. Innerhalb von Recipes kann über node
auf
die Attribute zugegriffen werden: node[:http_port]
.
Databags
node
-Attribute gelten immer im Kontext eines Servers. Es gibt aber auch
die Möglichkeit, globale Parameter geordnet abzulegen. Das geschieht in so
genannten Databags. In Databags können beliebige JSON-Daten global
gespeichert werden. Außerdem können sie in Recipes ausgewertet werden.
Angelegt werden sie typischerweise mit knife
. Der folgende Befehl erstellt
eine Sammlung von Benutzern:
Ein einzelner Benutzer kann nun zum Beispiel mit
angelegt werden. Entsprechend konfiguriert, öffnet knife
nun einen
Texteditor, in dem die JSON-Struktur für diesen Benutzer eingetragen werden
kann. Das id
-Attribut ist dabei schon vorbelegt, alles andere ist frei
wählbar, zum Beispiel:
Nach dem Schließen des Editors werden die Daten an den Chef-Server
übertragen. Innerhalb von Recipes kann nun auf diese Daten ähnlich wie auf
node
-Attribute zugegriffen werden: peter = data_bag_item('benutzer', 'peter')
.
Suchen
Attribute und Databags werden auf dem Server persistiert. Dadurch ist es
möglich, nach Nodes oder Databags zu suchen. Die Suchen können mit knife
aber auch innerhalb von Recipes durchgeführt werden. Zum Beispiel liefert
eine Liste aller Knoten, deren Name mit „test“ beginnt. Suchen sind nützlich, um Beziehungen zwischen verschiedenen Servern dynamisch zu verwalten. So kann zum Beispiel ein Webserver dynamisch eine Liste aller konfigurierten Application-Server erfragen und seine Lastverteilung entsprechend konfigurieren. Ebenso könnte ein Recipe die öffentlichen Schlüssel einer Reihe von Benutzern ermitteln und ihnen SSH-Zugang zum konfigurierten Rechner gewähren.
Teilen und Erweitern
Durch die Parametrisierbarkeit und das vergleichsweise hohe Abstraktionsniveau der DSL entsteht die Möglichkeit, wiederverwendbare Cookbooks zu schreiben. Tatsächlich existieren bei opscode [1] und auf GitHub schon zahlreiche Cookbooks für verschiedenste Anwendungsfälle. Aber auch die Sprache selbst ist erweiterbar. So ist es problemlos möglich, eigene Resource-Typen zu definieren, und bei Recipes handelt es sich um reine Ruby-Programme, in denen praktisch alles möglich ist. Chef ist also nicht einfach ein Tool, sondern ein Framework, das an die eigenen Bedürfnisse angepasst werden kann.
Ausblick
Dieser Artikel hat die grundlegenden Konzepte von Chef beleuchtet und gezeigt, dass Infrastructure as Code ein vielversprechender Ansatz ist. Allerdings soll nicht verschwiegen werden, dass er auch Risiken birgt: Der hohe Grad der Automatisierung erfordert auch automatisierte Tests, auf die hier nicht eingegangen wurde. Noch hat sich kein standardisiertes Vorgehen hierfür etabliert und auch die meisten öffentlich verfügbaren Cookbooks enthalten keine Tests. Als Einstieg ins Thema Testen können die Links [2] und [3] dienen.
Quellen, Links und Interessantes
- Chef-Webseite, http://www.opscode.com/chef/