Puppet und Co.

Wenn man einen (oder mehrere) Server betreibt, stellt sich häufig die Frage, wie man die Konfiguration dokumentiert, versioniert und reproduzierbar macht. Die Idee von Infrastructure as Code öffnet hier ganz neue Perspektiven.

Das Problem

Wer kenn nicht das Problem: man hat eine Software entwickelt und muss diese nun auf einen Server bringen oder möchte einen neuen Mailserver einrichten. Hierfür benötigt man aber eine ganze Reihe von Vorbedingungen auf dem Server: User müssen angelegt werden, eine Datenbank und andere Tools installiert und eine Unmenge von Konfigurationsdateien muss angepasst werden.

Da man nach dem DRY-Prinzip keinen manuellen Schritt zweimal machen sollte, beginnt man sehr schnell für das Setup des Servers Shell-Skripte zu schreiben und so ein gewisses Maß an Automatisierung zu erreichen. Parallel pflegt man eine Dokumentation, die beschreibt, was man alles geändert hat und in welcher Reihenfolge dies nötig ist. Häufig habe ich in der Vergangenheit auch versucht, die Konfigurationsdateien des Servers unter Versionsverwaltung (Perforce, GIT) zu stellen, damit man Änderungen später noch nachvollziehen kann.

Das hat aber alles immer nur mäßig funktioniert. Hierfür gibt es eine ganze Reihe von Gründen, wobei die folgenden wohl am gewichtigsten sind:

  • Die Shell-Skripte sind zwar nützlich, sie beschreiben aber die Schritte zum Ziel, nicht das Ziel selbst. So weiß man zwar, dass man mit useradd einen Benutzer anlegen will, dies versteckt sich aber in den Kommandos des Skripts.

  • Die Skripte können oft nicht mehrfach angewandt werden, weil sie die Umgebung verändern und dann beim zweiten Lauf ein anderes Verhalten zeigen. Um dies zu verhindern, muss man viel Zeit in die Entwicklung der Skripte stecken.

  • Die Dokumentation ist grundsätzlich immer veraltet, weil man doch vergisst eine Änderung in der Dokumentation nachzutragen. Außerdem ist sie getrennt von den Skripten und die beiden laufen gerne auseinander.

  • Versionierte Konfigurationsdateien sind praktisch, um Änderungen am System nachzuvollziehen, stellen aber die dritte Stelle dar, an der sich neben Skripten und Dokumentation Informationen über die Systemkonfiguration verstecken können.

Auch bei kommerzieller Software ist das Problem nicht unbekannt. Hier können die Installationshandbücher epische Dimensionen annehmen.

Infrastructure as Code

An diesem Punkt kommt die Idee von Infrastructure as Code ins Spiel: Warum betrachtet man die Konfiguration der Infrastruktur nicht einfach als etwas, das (genauso wie Software) durch Code beschrieben wird? Diesen kann man versionieren, er dokumentiert sich selbst und kann automatisch auf eine beliebige Anzahl von Servern angewandt werden. Zusätzlich beschreibt man nicht welche Schritte, in welcher Reihenfolge auf dem System durchgeführt werden müssen, sondern welchen Endzustand man erreichen möchte. Statt also useradd hugo abzusetzen sagt man einfach, dass ein Benutzer mit dem Namen hugo vorhanden sein muss. Gibt es ihn schon, muss nichts passieren, andernfalls wird er angelegt. Wenn man jetzt noch Abhängigkeiten zwischen verschiedenen Ressourcen (Benutzern, Dateien, installierter Software) definiert, kann ein Werkzeug vollautomatisch die nötige Schritte und deren Reihenfolge ableiten.

Auf den ersten Blick könnte man jetzt denken, dass Virtualisierung das hier beschriebene Problem lösen sollte, da man eine Maschine erstellt und dann beliebig oft klonen kann. Dies ist aber insofern nicht richtig, als

  • man auch die virtuelle Maschine in einen dokumentierten und definierten Zustand bringen möchte und nicht einfach wild installieren und zusammenbasteln und

  • die virtuellen Maschinen im laufenden Betrieb geändert werden müssen (z.B. für ein Software-Update) und man nicht jedes mal alle Maschinen neu klonen möchte.

Dieselbe Argumentation gilt auch für Betriebssystem-Images, die direkt auf der Hardware installiert werden.

Werkzeuge

Begibt man sich auf die Suche nach passenden Werkzeugen, um das Ziel der Infrastructure as Code zu erreichen, trifft man auf drei Kandidaten, die hinreichend verbreitet sind: Puppet, Chef und CFEngine. Nach Lektüre der Dokumentation und einiger Blog-Einträge ist meine Wahl auf Puppet gefallen. Die Hardware-Anforderungen für Chef schienen mir einfach zu hoch für meinen kleinen Testserver, CFEngine wurde oft als sehr komplex beschrieben, sodass letztendlich Puppet übrigblieb, das auch durch eine hervorragende Dokumentation glänzt. Die Entwicklungsumgebung Geppetto auf Basis von Eclipse erlaubt zudem das komfortable Entwickeln von Puppet-Dateien mit Syntax-Highlighting und Fehlersuche.

Puppet selber ist in Ruby geschrieben und sehr bescheiden bei den Systemanforderungen. Die verwendete DSL (Domänenspezifische Sprache) erschien mir sehr übersichtlich und gut verständlich. Weiterhin besteht die Möglichkeit, eigene Erweiterungen für Puppet zu programmieren - wovon ich aber erst einmal Abstand genommen habe. Es gibt aber eine ganze Reihe von fertigen Erweiterungen, die man von der Puppet-Webseite herunterladen kann.

Installation von Puppet

Mein Zielsystem ist ein CentOS 6.4, bei dem Puppet nicht zu der Standarddistribution gehört. Daher muss man zuerst das passende Repository bekannt machen, um dann Puppet mit yum zu installieren.

$ rpm -ivh http://yum.puppetlabs.com/el/6/
  products/i386/puppetlabs-release-6-7.noarch.rpm
$ yum install puppet

Puppet hat ein eigenes Modulsystem und erlaubt Erweiterungen und Plugins nachzuinstallieren. Für meinen Server benötigte ich vier solcher Erweiterungen, die mir die Konfiguration und Installation von MySQL und Apache mit PHP erleichtern sollten.

$ puppet module install puppetlabs/stdlib
$ puppet module install puppetlabs/mysql
$ puppet module install puppetlabs/apache
$ puppet module install thias/php

Will man eigene Module außerhalb des normalen Puppet-Pfades ablegen, muss man dies noch in der Datei /etc/puppet/puppet.conf einstellen:

[main]
modulepath = /etc/puppet/modules:/usr/share/puppet/modules:/root/puppet/modules

Da ich auch noch selbstgebaute RPMs verteilen möchte, habe ich zusätzlich einen Fileserver für Puppet in /etc/puppet/fileserver.conf konfiguriert:

[rpm]
    path /root/puppet/files
    allow *

Datenmodell von Puppet

Puppet fasst die Konfiguration einer Maschine (node bei Puppet) als eine Summe von Ressourcen auf. Für jede Ressource wird ein Zielzustand definiert, der dann von Puppet hergestellt wird. Wichtig ist, dass man jede Ressource nur einmal deklarieren kann, da man keine Operationen auf der Ressource, sondern deren Zustand angibt - und sie kann nur einen Zustand haben. Daher wäre das mehrfache angeben unsinnig und könnte im schlimmsten Fall sogar zu Widersprüchen führen.

Puppet unterstützt ab Werk eine ganze Reihe von Ressourcen, z.B. Dateien, installierte Pakete, Verzeichnisse, Benutzer, etc. Um beispielsweise das Paket dovecot zu installieren, wäre das entsprechende Puppet-Kommando:

package { "dovecot":
    ensure => latest,
}

Ein anderes Beispiel wäre das Anlegen eines Verzeichnisses mit entsprechenden Berechtigungen:

file { "/var/indexes":
    ensure => directory,
    owner   => "root",
    group   => "mailuser",
    mode    => '0600',
}

Die Konfiguration von Ressourcen kann man wieder zu Klassen zusammenfassen, die über Parameter gesteuert werden können. Auch hier gilt, dass eine Klasse nur ein einziges Mal pro Knoten genutzt werden kann.

class mysqldb ($root_password) {

    class { "mysql::server":
        config_hash => {
            "root_password" => $root_password,
        }
    }
}

Bei der Deklaration einer Klasse wird der Name ohne Anführungszeichen, bei der Verwendung mit angegeben.

Klassen werden dann zu der Konfiguration eines Knotens gruppiert:

node 'centos' {
    $www_root = "/srv/www"

   class { "apache2":
       www_root => "/srv/www2",
       hostname => "server.example.com",
   }

   class { "mysqldb":
       root_password => "foo"",
   }

   class { "dovecot": }
}

Viele Knoten zusammen ergeben eine Site.

Weitere Features

Puppte erlaubt es über eine Template-Sprache Konfigurationsdateien des Servers gemäß den Einstellungen einer Ressource anzupassen. Beispielsweise würde ein Template für eine .fetchmailrc wie folgt aussehen:

poll <%= @imap_server %>
   protocol IMAP
   user '<%= @imap_user %>'
   is <%= @username %>
   pass '<%= @imap_password %>'
   folder INBOX
   ssl
   mda 'formail -c >> ~/mail/.INBOX.<%= @imap_folder %>'
   keep

Die Platzhalter <%= @VARIABLE %> werden dann automatisch durch die entsprechenden Werte ersetzt. Kontrollstrukturen und Schleifen sind ebenfalls möglich.

Über ein File-Repoitory besteht die Möglichkeit, dass Dateien zentral vorgehalten werden und dann von den Knoten bei der Ausführung der Puppet-Anweisungen automatisch heruntergeladen.

Puppet unterstützt die Wiederverwendung von Klassen und Ressourcendefinitionen, sodass man die Konfiguration sehr schön modular aufbauen und gut pflegen kann. Zusammen mit einem Versionsverwaltungssystem bekommt man eine selbstdokumentierende und immer aktuelle Beschreibung der Systemkonfiguration ohne die Qual der althergebrachten Methoden.

Zusammenfassend kann man sagen, dass Puppte bzw. Infrastructure as Code ein gewaltiger Fortschritt bei der Konfiguration von Servern und Rechnern ist.