Schneller als Licht (Linux-Magazin, Oktober 2015)

Das Provisionierungstool Ansible eignet sich nicht nur zum Konfigurations- und Releasemanagement mittelgroßer Serverfarmen, sondern auch für den Hausgebrauch zum Restaurieren von Anpassungen auf dem Linux-Desktop daheim.

Etwa alle fünf Jahre ist es in den Perlmeister-Labs an der Zeit, eine neue Linux-Distro zu installieren. Nur alle fünf Jahre deshalb, weil es meiner Erfahrung nach mit einem Wahnsinnsaufwand verbunden ist, nach dem "do-release-upgrade" all die kleinen Cronjobs und Webapplikationen wieder zum Laufen zu bringen, die ein solcher Upgrade wegen neuer Versionen von Perl, Apache und diverser Libs anscheinend zwangsläufig mit sich bringt. Viele Open-Source-Entwickler, sowohl in der Distro-Szene (hallo, Ubuntu!) als auch im Tool-Bereich (hallo, Openssl!) scheren sich einen feuchten Kehricht um Backwards-Compatibility und ich darf mir jedes Mal ein paar Tage Urlaub nehmen, um fluchend an meinem Linux-Desktop zu sitzen und Konfigurationen zu ändern oder Skripts anzupassen. Und oft erinnere ich mich dann nur noch vage an all die vielen kleinen von Hand eingefügten Patches, die notwendig waren, um das ein oder andere Tool endlich zur Zufriedenheit zum Laufen zu bringen. Auch Wochen nach dem Upgrade ist es nicht ungewöhnlich, etwas zu finden, das nicht ordnungsgemäß funktioniert und ich zermartere mir oft das Hirn, um herauszufinden, wie ich das vor Jahren nochmal gelöst habe.

Handbetrieb lachhaft

Server von Hand einzurichten löst ja im Profibereich schon seit vielen Jahren Lachkrämpfe aus, und Werkzeuge wie Puppet oder Chef erledigen dort Konfiguration und neue Releases automatisch und zuverlässig. Wer seine Änderungen in die versionierten Konfigurationsdateien dieser Tools einträgt hat zudem den Vorteil, dass alles jederzeit fehlerfrei reproduzierbar ist. Leider kommt jedes dieser Tools mit einer kultartigen Religion daher, deren Jünger sich anscheinend lebenslang verpflichten müssen, kostenpflichtige Trainings zu absolvieren, Zertifizierungen abzulegen, und ja nicht von der verbotenen Frucht zu naschen, nach deren Verzehr der Abtrünnige sich dann vielleicht fragen würde, ob es tatsächlich sinnvoll ist, sich vom kreativen Skripthacker zum willfährigen Anpasser von Konfigurationsdateien nach einem vorgegebenen Schema umschulen zu lassen.

Deswegen freute es mich während der Lektüre des relativ neuen und sehr empfehlenswerten Ansible-Buches [2], zu erfahren, dass dieses freie Provisionierungswerkzeug (https://github.com/ansible/ansible) nicht nur keinen Agenten auf dem Zielsystem erfordert, weil es sich einfach mittels ssh dorthin verbindet, sondern auch nach unten skaliert, also auch für die rechenzentrumslose Oma Meume ohne viel Brimborium einen einzigen Desktop verwalten kann.

Ursprung Science Fiction

Der Name Ansible kommt übrigens aus der Science-Fiction-Literatur und bezeichnet ein Gerät, mit dem man mit Überlichtgeschwindigkeit über Galaxieentfernungen hinweg völlig verzögerungsfrei kommunizieren kann. Wikipedia kommentiert jedoch lapidar, dass unsere Wissenschaftler zur Zeit noch nicht wissen, wie ein solches Gerät zu bauen wäre. Außerdem kann man zwar der Ansible-Religion beitreten und seine Anweisungen deklarativ im Yaml-Format schreiben, aber auch genauso gut handgestrickte Perl-Skripts ablaufen lassen.

Modernen Linux-Distros liegt das Tool als Paket bei. Unter Ubuntu installiert es sich zum Beispiel mit

    sudo apt-get install ansible

und mit dem Ping-Modul und dem heimischen Desktop localhost als Ziel aufgerufen bestätigt es, dass alles in Ordnung ist und nichts verändert wurde:

    $ ansible localhost -m ping
    localhost | success >> {
        "changed": false, 
        "ping": "pong"
    }

Nun zur Praxis: Eine einfache, aber ständig wiederkehrende Aufgabe nach der Neuinstallation meines Systems zuhause ist es zum Beispiel, eine maßgeschneiderte .vimrc-Datei ins Home-Verzeichnis zu platzieren. Glücklicherweise liegt diese öffentlich zugänglich in einem meiner Repositories auf Github (Abbildung 1), aber Ansible kann jetzt das Herunterladen automatisieren und gibt der Datei nach der Installation auch gleich noch die voreingestellten Nutzerrechte. Und da die die Datei, die den Wunschzustand des Systems beschreibt, zusammen mit einer Vielzahl anderer Ansible-Skripts in einem Verzeichnis liegt, ist die Wiederherstellung der Vim-Konfiguration nun fester Bestandteil der Systemrestauration, den garantiert kein schussliger Admin vergisst.

Abbildung 1: Auf Github steht die maßgeschneiderte .vimrc-Datei des Autors.

Spiel mit

Seine Instruktionen bezieht Ansible aus sogenannten Playbooks, die im Yaml-Format beschreiben, welche Kommandos auszuführen sind und welche Dateien diese modifizieren, sodass es später feststellen kann, ob eine Anweisung schon vorher erfolgt ist und keiner neuen Anstrengung mehr bedarf. Ruft man Ansible also zweimal hintereinander auf, sollte der zweite Aufruf ganz schnell ohne Änderungen zurückkehren. Diese Idempotenz ist sehr hilfreich. Um zum Beipiel die besagte Datei .vimrc zu installieren, nutzt das Playbook in Listing 1 das standardmäßig mit ausgelieferte Ansible-Modul get_url, das laut Dokumentation mindestens die Parameter url (Download-URL einer Datei), dest (der Name der lokal installierten Datei) und mode (die Unix-Nutzerrechte) erwartet. Dieses Playbook läuft auf allen Linux-Distros und Plattformen, die Ansible unterstützt, hilft also auch auch unterwegs auf meinen Macbook. Das Modul get_url wird übrigens bei einem zweiten Lauf in der Standardeinstellung keinen erneuten Download einleiten, falls die lokale Datei bereits existiert. Wer stetig auffrischen möchte, wenn neue Versionen vorliegen, weist Ansible mit dem zusätzlichen Parameter force=yes an, die Datei bei jedem Aufruf des Plays vom Internet zu laden und die lokale Version nur dann zu ersetzen, falls sie von der frisch eingetroffenen abweicht.

Listing 1: vimrc.yml

    01 ---
    02 - name: Dev Tools Setup
    03   hosts: localhost
    04   tasks:
    05     - name: Vimrc from Github dotfiles
    06       get_url: >
    07         url=https://raw.githubusercontent.com/mschilli/dotfiles/master/.vimrc
    08         mode=0644
    09         validate_certs=no
    10         dest=~/.vimrc

Weiter steht in Listing 1 mit localhost der Zielhost, und die Yaml-Datei hat noch viel Platz für mehrere solcher "Plays", die jeweils aus verschiedenen "Tasks" bestehen, denen jeweils mit name ein Name zugeordnet ist, der bei der Ausführung mit ansible-playbook vimrc.yml wie in Abbildung 2 gezeigt darüber Auskunft gibt, was gerade passiert und im Falle eventuell auftretender Fehler bei der Ursachensuche hilft. Die Ausgabe zeigt an, dass .vimrc ohne Probleme heruntergeladen und installiert wurde, und dass sich deswegen der Status des Linux-Desktopcomputers verändert hat. Die längliche Github-URL zum direkten Laden der neuesten Version der dort eingecheckten Datei fand ich übrigens im Browser auf github.com unter dem "Raw"-Button.

Abbildung 2: Ansible lädt eine .vimrc-Datei aus einem Github-Repository und installiert sie lokal.

Altes Skript in neuem Gewand

Als nächstes steht beim Anpassen einer Linux-Distribution typischerweise ein Lauf meines Skripts binlinks an, das seinerseits in einem lokal geklonten Git-Repository liegt und eine Vielzahl weiterer praktischer Perlskripts, die ihrerseits in Unterverzeichnissen des gleichen Repos liegen, mit Einträgen im bin-Verzeichnis unter meinem Home-Directory verlinkt. Mein Perlskript-Template-Generator tmpl mit dem ich beinahe jedes Projekt starte, ist so zum Beispiel einfach von der Kommandozeile als tmpl verfügbar, da mein Kommandopfad $PATH das Verzeichnis bin in meinem Homeverzeichnis mit einschließt.

Listing 2: binlinks

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use File::Basename;
    04 use Sysadm::Install qw(mkd);
    05 
    06 my($home)    = glob "~";
    07 my $home_bin = "$home/bin";
    08 
    09 while(<DATA>) {
    10   chomp;
    11 
    12   my($linkbase, $src) = split ' ', $_;
    13 
    14   $src        = "$home/$src";
    15   my $binpath = "$home_bin/$linkbase";
    16 
    17   if(-l $binpath) {
    18     warn "$binpath already exists";
    19     next;
    20   } elsif (-e $binpath) {
    21     warn "$binpath already exists, " .
    22       "but not a link!";
    23     next;
    24   }
    25 
    26   if(! -d dirname($binpath)) {
    27     mkd dirname($binpath);
    28   }
    29 
    30   symlink $src, $binpath or
    31     die "Cannot link $binpath->$src ($!)";
    32 }
    33 
    34 __DATA__
    35 binlinks git/myrepo/binlinks/binlinks
    36 tmpl git/myrepo/tmpl/tmpl

Listing 3: binlinks.yml

    1 ---
    2 - name: Tools setup part 2
    3   hosts: localhost
    4   tasks:
    5     - name: binlinks script
    6       command: ./binlinks

Wie Listing 2 zeigt, erzeugt das Skript einfach für jedes unterhalb des __DATA__-Bereichs am unteren Ende aufgelistete Skript einen Symlink, der auf die Position des Originals im Repository zeigt. Der Aufruf ansible-playbook binlinks.yml lässt das Playbook in Listing 3 mit dem Ansible beiliegenden command-Modul ablaufen, das wiederum das Skript binlinks aufruft und mit einer Erfolgsmeldung zurückkehrt, falls das Skript seinerseits mit dem Returncode 0 abschließt.

Mit einer Kombination aus dem von Ansible unterstützten Template-Mechanismus zum Modifizieren von Dateien und weiteren Skripts kann ich nun auch kompliziertere Setups wiederholen, wie zum Beispiel die bislang bei jedem Upgrade zerstörte Nagios-Konfiguration. Auch wenn selbst Ansible nicht hellsehen kann und nur die anno dazumals ausgeführten Schritte wiederholt, hilft es doch ungemein, wenigstens zu wissen, welche Eingriffe zum Installieren notwendig waren, damit man sie später wenn auch leicht angepasst mit Ansible wiederholen kann.

Virtuelle Welten

Eine weitere Konfigurationsorgie steht an, wenn man zum Beispiel ausprobieren möchte, ob ein gerade in der Entwicklungsumgebung fertiggestelltes Perlmodul auch auf einem Produktionssystem mit Apache2-Server und mod_perl2-Beschleuniger fehlerfrei läuft. Am besten eignet sich dazu ein Reinraum, der ausgehend von einer blanken Linux-Distro in einer Vagrant-VM (vagrantup.com und [3]) nur die absolut notwendigen Pakete installiert, den Server entsprechend konfiguriert und das Perl-Modul ebenfalls hineinpflanzt. Damit sich der Entwickler bei diesem Verfahren nicht die Finger wund tippt, liegen die Instruktionen hierfür maschinenlesbar im Source-Repository und Ansible führt sie bei jeder neu erzeugten VM genau gleich aus. Das Vagrantfile in Listing 4 setzt auf einer aktuellen Ubuntu-Distro auf, die sich vagrant aus dem Internet pumpt, falls sie noch nicht in der lokalen VirtualBox-Installation bereitliegt. Weiter sorgt die Direktive forwarded_port dafür, dass der innerhalb der VM auf Port 80 lauschende Apache-Server auf dem Hostsystem unter Port 8080 lauert.

Listing 4: Vagrantfile

    1 VAGRANTFILE_API_VERSION = "2"
    2 
    3 Vagrant::configure(VAGRANTFILE_API_VERSION) do |config|
    4 
    5     # 32-bit Ubuntu Box
    6   config.vm.box = "ubuntu/trusty64"
    7   config.vm.network "forwarded_port", guest: 80, host: 8080
    8 end

Zunächst muss Ansible allerdings erst einmal wissen, unter welchem SSH-Port und mit welchem privaten Schlüssel es sich mit der frisch mittels vagrant up hochgefahrenen VM verbinden kann. Der Aufruf von vagrant ssh-config offenbart, dass die Ubuntu-VM über ssh-Port 2222 auf dem lokalen Host erreichbar ist und unter welchem Pfad der private SSH-Schlüssel zur Identifizierung auf der Festplatte liegt. Die Ansible-Konfigurationsdatei in Listing 5 setzt diese Werte für Ansible und stellt damit sicher, dass das Tool sich in der neu erzeugten VM einloggen und dort die vordefinierten Arbeiten verrichten kann. Wird ansible-playbook aus dem gleichen Verzeichnis aufgerufen, stellt es zum Verbindungsaufbau die in der Konfigurationsdatei ansible.cfg in Listing 5 festgelegten Werte ein und fährt damit gut.

Listing 5: ansible.cfg

    1 [defaults]
    2 hostfile = hosts
    3 remote_user = vagrant
    4 private_key_file = /home/mschilli/.vagrant.d/insecure_private_key
    5 host_key_checking = False

Stetige Kontrolle

Bei neu geschriebenen oder aufgrund von Bugfixes leicht modifizierten Softwareprojekten stellt sich immer die Frage, ob das System nach dem Fix auch noch funktioniert. Eine Unit-Testsuite räumt schon mal besonders peinliche Fehler aus dem Weg, aber letztendliche Klarheit verschafft nur der Test in einer Produktionsumgebung. Ob der mod_perl-Handler in Listing 6 zum Beispiel funktioniert, ist mit Sicherheit unter einem Apache-Server mit installiertem mod_perl-Modul zu sagen. Der Entwickler müsste dazu nicht nur die entsprechenden Distro-Pakete wie apache2 und mod-perl2 installieren, sondern auch noch den Webserver unter dem Pfad /modperltest mittels einer Konfigurationsdatei wie in Listing 7 auf mod_perl einnorden.

Listing 6: ModPerlTest.pm

    01 package ModPerlTest;
    02 use strict;
    03 use warnings;
    04 
    05 use Apache2::RequestRec ();
    06 use Apache2::RequestIO ();
    07 use Apache2::Const -compile => qw( OK );
    08 
    09 sub handler {
    10     my( $r ) = @_;
    11 
    12     $r->content_type( 'text/html' );
    13     print "<H1>Ansible did it!</H1>\n";
    14 
    15     return Apache2::Const::OK;
    16 }
    17 
    18 1;

Listing 7: mod-perl-test.conf

    1 <Location /modperltest>
    2   SetHandler perl-script
    3   PerlResponseHandler ModPerlTest
    4 </Location>

Ansible hilft auch hier. Listing 8 zeigt das Playbook für einen Apache2-Server mit mod_perl2-Modul und der Testapplikation (Listing 6) und die das Playbook beim Aufruf von ansible-playbook mod-perl.yml in der vorher mittels vagrant up hochgefahrenen VM installiert. Der Parameter apt gibt mit den Schlüsseln name die Namen der Ubuntu-Pakete an, die es zu installieren gilt. Der Parameter update_cache=yes veranlasst Ubuntu vorher dazu, die neuesten Indexdateien der Repositories einzuholen, was zwar enorm Zeit kostet, aber bei einer frischen Installation unumgänglich ist, da der Cache einer frischen Ubuntu-VM immer veraltete Informationen birgt, die dazu führen, dass ein nachfolgendes apt-get install oft die benötigten Pakete nicht findet.

Da Ansible normalerweise nicht als root läuft, das Kommando apt-get install aber Root-Rechte in der VM benötigt, aktiviert Zeile 7 in Listing 8 den Sudo-Modus. So kann jede Task ihre Rechte feinjustieren und nichts läuft unnötigerweise mit gefährlich viel Feuerpower. Außerdem kopiert das Playbook in Listing 8 die Apache-Konfigurationsdatei aus Listing 7 nach /etc/apache2/conf, damit der Aufruf von http://localhost:80/modperltest innerhalb der VM oder dank des Port-Mappings von Listing 4 auf Port 8080 auf dem Hostrechner die Mod-Perl-Applikation anspringt, die die Ausgabe in Abbildung 3 im Browser erzeugt. Weiter sorgt Ansible dafür, dass der Apache-Server neu startet, falls sich seine Konfiguration oder der Source-Code der Applikation geändert hat. Die Direktive notify: restart apache aus Zeile 11 löst immer dann den ab Zeile 18 definierten Restart-Mechanismus via apachectl aus, wenn der Entwickler eine neue Version der Applikation einspielt.

Listing 8: mod-perl.yml

    01 ---
    02 - name: mod_perl Test Application
    03   hosts: testserver
    04   tasks:
    05     - name: Apache and mod_perl
    06       apt: name=apache2,libapache2-mod-perl2 update_cache=yes
    07       sudo: True
    08     - name: Test script
    09       copy: src=ModPerlTest.pm dest=/usr/lib/perl5/ModPerlTest.pm
    10       sudo: True
    11       notify: restart apache
    12     - name: mod_perl conf
    13       copy: >
    14         src=mod-perl-test.conf 
    15         dest=/etc/apache2/sites-enabled/mod-perl-test.conf
    16       sudo: True
    17   handlers:
    18     - name: restart apache
    19       command: sudo apachectl restart

Abbildung 3: In der Vagrant-VM läuft dank Ansible mod_perl2 auf apache2 mit dem Testskript aus Listing 4.

Fit für 2020

Mit diesen Skripts wird der Upgrade auf Ubuntu 21.04 im Jahre 2020 zweifelsohne wie am Schnürchen klappen. Da die Ansible-Instruktionen in einem Git-Repository liegen, können sie über die kommenden Jahre ständig gepflegt und erweitert werden, und beim nächsten Upgrade wird auch garantiert nichts versehentlich ausgelassen. Voraussetzung ist natürlich, dass Ansible in dem 2020 vermutlich aktuellen Release 7.42 noch die Syntax der Version 1.5.3 von anno 2015 beherrscht, was ja auch nicht selbstverständlich ist!

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2015/10/Perl

[2]

"Ansible: Up and Running", Lorin Hochstein, O'Reily 2015

[3]

"Entspannt nach draußen", Michael Schilli, Linux-Magazin 05/2013, http://www.linux-magazin.de/Ausgaben/2013/05/Perl-Snapshot

Michael Schilli

arbeitet als Software-Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen der Skriptsprache Perl. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.