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.

Abbildung 1: Chef Client/Server

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.

package "tar" do
  # Alternativen: upgrade, remove, purge

  action :install
  # Wahl eines Providers statt des Defaults

  provider Chef::Provider::Package::Rpm
end
Listing 1: Package Resource

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).

<body>
    Hallo <%=@Name%>. Ich zähle bis 10:
    <ul>
        <% (1..10).each {|i| %>
            <li> <%=i%> </li>
        <% } %>
    </ul>
</body>
Listing 2: beispiel.erb
### Es wird eine Datei mit dem Namen /tmp/beispiel.html

template "/tmp/beispiel.html" do
  source "beispiel.erb"
  variables(
    :Name => "Welt"
  )
end
Listing 3: Template Resource

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:

templates
    eigenbrodt.example.org/beispiel.erb
    ubuntu-11.04/beispiel.erb
    ubuntu/beispiel.erb
    default/beispiel.erb

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:

$ knife node list
testServer
testServerZwei

Mit knife können wir auch noch mehr Informationen über einen speziellen Knoten abrufen:

$ knife node show testServer
Node Name: testServer
Environment: _default
FQDN: ip-10-56-66-177.eu-west-1.compute.internal
IP: 46.137.49.126
Run List: recipe[exampleRecipe]
Roles:
Recipes: exampleRecipe
Platform: ubuntu 11.04

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:

knife node run_list add testServer recipe[tomcat]

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):

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:

Ü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].

node.default["http_port"] = "80"
### Node muss nicht unbedingt angegeben werden

default["ssh_port"] = "22"
Listing 4: attributes/attribute.rb

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:

knife data bag create benutzer

Ein einzelner Benutzer kann nun zum Beispiel mit

knife data bag create benutzer peter

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:

{
 "id": "peter",
 "public_key": "ssh-rsa AAAAB3NzaC1yc2EAA...."
}

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

knife search node 'name:test*'

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

Referenzen

  1. http://community.opscode.com/cookbooks  ↩

  2. http://www.cucumber-chef.org/  ↩

  3. http://vagrantup.com/  ↩