Quadratisch Praktisch (Linux-Magazin, Mai 2014)

Das brandheiße Docker-Projekt fährt gleichzeitig mehrere Linux-Distros auf einem Rechner ohne den Überhang voller Virtualisierung und erlaubt es dem Perlmeister, seine Module plattformübergreifend zu testen.

Virtualisierung war gestern. Statt die gesamte Hardware samt Operationssystem zu abstrahieren, baut das Docker-Projekt auf den in neueren Linux-Kerneln vorhandenen Support für Linux-Container (LXC) auf und isoliert diese auf Prozessebene. Verkaufsargumente sind drastische Einsparungen beim verbrauchten Speicher und Performance-Gewinne um Größenordnungen. Wenn jeder Rechner plötzlich mühelos tausende voneinander abgekapselte Applikationen laufen lassen kann, eröffnen sich ganz neue Möglichkeiten im Rechenzentrum.

Das Docker-Projekt (http://docker.io und [2]) setzt auf den LXC-Features neuerer Linux-Kernels auf und fährt einen Dämon hoch, der alle Docker-Container verwaltet. Er läuft auf dem Host-System, aber gerne auch in einer VM. Wer also noch ein älteres System fährt, das Docker noch nicht im Kernel unterstützt, kann einfach mit Vagrant ([3]) zum Beispiel ein Ubuntu-13-Image erzeugen und dort Docker mit den Anweisungen aus [4] installieren.

Im Perl-Snapshot testen verschiedene Container heute, ob sich der Tarball eines CPAN-Moduls auf verschiedensten Linux-Distos installieren lässt.

Mehr als nur LXC

Docker-Container bieten viel mehr als LXC allein, angefangen von einem standartisierten Applikationsformat, das es erlaubt, auf einem Linux-System definierte Container auf allen anderen Systemen laufen zu lassen, die ebenfalls Docker unterstützen. Weiter arbeitet Docker mit geschichteten Dateisystemen, sodass eine Applikation auf einem Base-Image aufsetzt, und nur die Differenzen definiert, die Docker auch noch mit Git-ähnlicher Präzision versioniert. Docker definiert auch noch eine Netzwerkschnittstelle zwischen den Containern, so dass die darin laufenden Applikationen über Unix-Ports miteinander kommunizieren können. Und dass User einmal definierte Docker-Container in einem öffentlichen (oder wahlweise auch privaten) Repositories zur Weiterverwendung ablegen können, gibt der Docker-Community eine gute Portion Schwung. Das Projekt ist gut ein Jahr alt und seit Version 0.8 laut Entwicklern produktionsreif.

Der Hauptvorteil isolierter Container ist freilich die Entkoppelung der Komponenten. Nutzen zwei Applikationen zum Beispiel die gleiche Library, brauchen aber unterschiedliche Versionen, ist dies kein Hinderungsgrund, denn jeder Container bringt alles notwendige selbst mit.

Alles neu macht der Mai

Einen neuen Container holt der User zunächst als Basisversion aus dem öffentlichen Repository und steigt dann hinein, um ihn entsprechend den Anforderungen einzurichten. So angelt docker pull ubuntu ein etwa 200MB großes Ubuntu-Image vom Netz und

    # docker run -i -t ubuntu /bin/sh

öffnet dann eine Shell im Container, in der der User mit "apt-get update" und "apt-get install" nach Herzenslust neue Ubuntu-Pakete installieren kann. Die Option -i legt den interaktiven Modus fest, -t öffnet ein Pseudo-Terminal zum Bedienen der Shell. Zur maschinellen Erstellung von Containern hingegen dient eine Datei namens Dockerfile wie in Listing 1.

Listing 1: Dockerfile

    1 FROM ubuntu
    2 
    3 RUN apt-get -y update
    4 RUN apt-get -y install cpanminus
    5 RUN apt-get -y install make
    6 RUN apt-get -y install libwww-perl

Sie legt mit der Direktive FROM fest, von welchem Basis-Image der Container abzuleiten ist (in diesem Fall ubuntu). Neben dem Standard-Image fördert das Kommando docker search ubuntu noch 683 weitere Variationen zutage, die ebenfalls zum Download bereitstehen. Die mit dem Schlüsselwort RUN eingeleiteten Zeilen im Dockerfile geben Kommandos ein, mit denen docker den Container anpassen soll. Nach einem apt-get update zum Auffrischen des Paket-Index installieren die Kommandos im Perltest-Container noch die Pakete libwww-perl zum Herunterladen von CPAN-Modulen und die Utility make zum Bauen und Testen derselben.

CPANminus als Plus

Das Paket cpanminus bringt die Utility cpanm mit, die nicht nur CPAN-Module testen und installieren kann, sondern auch Distributions-Tarbälle entpackt und deren Unit-Test-Suite startet. Das ist nicht immer trivial, denn Perl-Module definieren oft Abhängigkeiten zu anderen CPAN-Modulen, die erst einmal eingeholt, getestet und installiert werden müssen bevor die Installation des eigentlichen Moduls beginnen kann. Alle heute vorgestellten Container installieren diese Utilty entsprechend den Erfordernissen der dort laufenden Linux-Distribution, damit das Testskript, das den Tarball in allen konfigurierten Containern testet, dort jeweils ein einheitliches Kommando aufrufen kann.

Listing 2: Dockerfile

    1 FROM base/arch
    2 
    3 RUN pacman -S --noconfirm tar make community/cpanminus
    4 RUN ln -s /usr/bin/vendor_perl/cpanm /usr/bin/cpanm

Schweigen ist Gold

Wichtig ist, dass die im Container ausgeführten Kommandos keinerlei Rücksprache mit dem User halten. Besteht nämlich keine interaktive Verbindung mit dem Container durch ein Pseudo-Terminal, führen Rückfragen automatisch zu einem Fehler und zum Abbruch des Kommandos. So frägt die Utilty apt-get auf Ubuntu zum Beispiel immer mit "Do you want to continue [Y/n]?" nach, bevor sie zur Installation eines angeforderten Pakets schreitet, und erwartet, dass der User y eingibt. Das Dockerfile in Listing 1 ruft apt-get deshalb mit der Option -y auf, das die Abfrage unterbindet und stattdessen gleich loslegt.

Das Dockerfile in Listing 2 hingegen definiert den Inhalt des Perltest-Containers für Arch Linux. Dessen Docker-Image liegt unter base/arch im öffentlichen Repo. In der Distro heißt der Paket-Manager pacman und er führt die Installation von Paketen normalerweise nur aus, wenn der User auf die Frage "Proceed with installation? [Y/n]" mit "y" antwortet. Mit der Option --noconfirm geht er stattdessen gleich ans Werk.

Zwar kommt Arch Linux von Haus aus mit einem Perl-Binary daher, aber es fehlen die Utilities tar und make. Das cpanm-Skript ist im Arch-Paket community/cpanminus enthalten, also zieht das Dockerfile diese über pacman beim Einrichten des Containers heran.

Allerdings installiert Arch Linux das Skript cpanm im Pfad /usr/bin/vendor_perl, der sich nicht in der $PATH-Variablen der Shell findet. Da das Testskript später aber cpanm ohne Pfadangabe aufruft, erzeugt das zweite RUN-Kommando mit ln -s kurzerhand einen symbolischen Link unter /usr/bin. Dort sucht die Shell unter Arch Linux standardmäßig nach Kommandos, und somit ist Plattformparität sicher gestellt.

Abbildung 1: Jedes Plattform-Verzeichnis enthält ein C mit Instruktionen zum Erstellen des Containers.

Einrichtung festzementiert

Die zwei bislang vorgestellten Container-Konfigurationen zum Testen von Perl-Modulen (und etwaige weitere) erwartet das Skript conprep zur Containervorbereitung in Listing 3 nun unterhalb des Verzeichnisses containers jeweils in einem Unterverzeichnis mit dem Namen der Distro ("arch" bzw. "ubuntu", siehe Abbildung 1). Es springt nach dem Aufruf ins Verzeichnis mit dem Dockerfile der Distro und ruft dort das Kommando

    # docker build --no-cache .

auf. Der abschließende Punkt steht für das aktuelle Verzeichnis, in dem sich die festgezimmerte Konfiguration befindet. Die Option --no-cache bestimmt, dass dies auch wirklich Schritt für Schritt passiert und Docker keine Abkürzung über eventuell schon auf dem Host zwischengespeicherten Images von früheren Installationen nimmt. Dabei holt Docker jeweils das entsprechende Image aus dem Repo und führt die angegebenen Kommandos der Reihe nach aus.

Container suchen

Damit aus einem heruntergeladenen Image ein Container wird, muss docker run laufen, was implizit mit den im Dockerfile aufgelisteten RUN-Direktiven passiert. Falls sich dort keine befänden, würde der beispielsweise der Aufruf docker run -i -t ubuntu ls dafür sorgen, dass tatsächlich ein Container entsteht und docker nach der Ausführung des ls-Kommandos aus dem Container herausspringt und die Kontrolle zur Shell des Hosts zurückgibt.

Aber wo findet sich nun der entstandene Container? Die Ausgabe der Docker-Kommandos verrät darüber überraschenderweise nichts, dort stehen nur die Image-IDs, nicht die Container-IDs, die man braucht, um in letztere einzusteigen und dort Kommandos auszuführen. Zu Hilfe kommt das Kommando docker ps, das alle laufenden Container mit ihren IDs anzeigt, und mit der Option -l gibt es nur den als letzten erzeugten an (Abbildung 2).

Abbildung 2: Das Kommando C findet den letzten erzeugten Container.

Die Hex-Zahl in der ersten Spalte ist die ID des Containers. Diese genügt aber ebenfalls nicht, um in den Container hinein zu springen. Vielmehr besteht Docker darauf, dass diese ID nun mit dem Kommando docker commit id name festgezimmert und einem Namen zugewiesen wird. Erst mit diesem Namen kann der User dann mit

    docker run -i -t name /bin/sh

in den Container einsteigen. Das Skript in Listing 1 vollführt den vorgeschriebenen Regentanz und weist jedem neuen Container den Namen distname-perltest zu, wobei distname für den Namen der verwendeten Distro (z.B. "ubuntu") steht.

Listing 3: conprep

    01 #!/usr/bin/perl -w
    02 use strict;
    03 use Sysadm::Install qw(:all);
    04 use Log::Log4perl qw( :easy );
    05 use File::Basename;
    06 use Getopt::Std;
    07 
    08 Log::Log4perl->easy_init($DEBUG);
    09 
    10 for my $container_path ( <containers/*> ) {
    11   cd $container_path;
    12   my $container = basename $container_path;
    13 
    14   tap { raise_error => 1 },
    15     "docker", "build", "--no-cache", ".";
    16 
    17   my( $stdout, $stderr, $rc ) = 
    18     tap "docker", "ps", "-l";
    19 
    20   my @lines = split /\n/, $stdout;
    21 
    22   if( $lines[1] =~ /^(\w+)/ ) {
    23       my $container_id = $1;
    24       tap { raise_error => 1 }, 
    25         "docker", "commit", $container_id,
    26         "$container-perltest";
    27   } else {
    28       die "unexpected format: @lines";
    29   }
    30 
    31   cdback;
    32 }

Zeile 20 trennt die Ausgabezeilen des über tap() ausgeführten Kommandos docker ps -l , und Zeile 22 schnappt sich die zweite und letzte Zeile und extrahiert die am Zeilenanfang stehende Hex-ID. Das Commit-Kommando in Zeile 25 zimmert die Version unter dem angegebenen Namen fest. Die Funktion cdback (ebenfalls aus Sysadm::Install) springt am Ende der for-Schleife wieder zurück ins ursprüngliche Verzeichnis, damit das nächste cd-Befehl in ein relativ angegebenes Verzeichnis erfolgreich vonstatten geht.

Nach getaner Arbeit steht nun für jede Distro ein Container zur Verfügung. Abbildung 3 zeigt, wie Docker zum Beispiel eine Shell im Arch-Linux-Container öffnet und die installierte Version bestätigt.

Abbildung 3: Der User öffnet eine Shell im Arch-Linux-Container und verifiziert die Distro-Version.

Rauch, steig auf

Das Skript smoke-me in Listing 4 nimmt als Argument den Tarball eines Perl-Moduls (entweder vom CPAN heruntergeladen oder per "make tardist" erzeugt) und orgelt wie vorher conprep durch alle Distro-Verzeichnisse, prüft aber, ob sich das Modul problemlos in der jeweiligen Distro installieren lässt.

Listing 4: smoke-me

    01 #!/usr/bin/perl -w
    02 use strict;
    03 use Sysadm::Install qw(:all);
    04 use Log::Log4perl qw( :easy );
    05 use File::Basename;
    06 use Getopt::Std;
    07 use File::Temp qw( tempdir );
    08 
    09 getopts( "v", \my %opts );
    10 
    11 my( $tarball_path ) = @ARGV;
    12 die "usage: $0 tarball" if 
    13     !defined $tarball_path;
    14 
    15 my $tempdir = tempdir( CLEANUP => 1 );
    16 cp $tarball_path, $tempdir;
    17 my $tarball = basename $tarball_path;
    18 
    19 my $log_level = 
    20     ( $opts{ v } ? $DEBUG : $INFO );
    21 
    22 Log::Log4perl->easy_init( $log_level );
    23 
    24 for my $container_path ( <containers/*> ) {
    25   cd $container_path;
    26   my $container = basename $container_path;
    27 
    28   my @verbose = 
    29     ( $opts{ v } ? ("-v") : () );
    30 
    31   my $rc =
    32   sysrun "docker", "run", "-i", 
    33     "-v", "$tempdir:/mnt/tmp", 
    34     "$container-perltest", 
    35     "cpanm", @verbose, "/mnt/tmp/$tarball";
    36 
    37   INFO "Test in container '$container' ",
    38     ( $rc ? "failed" : "OK" );
    39 
    40   cdback;
    41 }

Wie aber gelangt nun der Tarball des zu testenden Perl-Moduls vom Host-System in den Container? Mit der Option

    docker run -v "/dir1:/dir2" -i -t cmd

aufgerufen erstellt Docker im Container einen Mount /dir2, der auf das Verzeichnis /dir1 auf dem Hostsystem zeigt. Das Skript in Listing 4 kopiert den Tarball auf dem Hostsystem zunächst mit der Funktion "cp" (aus Sysadm::Install) in ein neu angelegtes temporäres Verzeichnis und teilt dann Docker mit, dass es letzteres unter /mnt/tmp im Container vorzufinden wünscht. Prompt findet cpanm später im Container dort den Tarball. Die Option "-v" (für verbose) des Skripts schnappt Zeile 29 auf und reicht sie zum Aufruf von cpanm im Container durch. Die Funktion sysrun (ebenfalls aus Sysadm::Install) sorgt dann dafür, dass die Ergebniss der durchlaufenden Tests in Echtzeit angezeigt werden. Abbildung 4 zeigt den Ablauf der Tests in zwei verschiedenen Containern in der weniger gesprächigen Version ohne -v.

Abbildung 4: Der Tarball des Log4perl-Moduls absolviert seine Testsuite in den Arch-Linux- und Ubuntu-Containern.

Offensichtlich traten keine Probleme auf, und das Modul wurde erfolgreich auf zwei Linux-Distros zertifiziert. Noch ein Trick: Wenn sich zuviele gar nicht mehr laufende Docker-Container angesammelt haben (docker ps -a zeigt sie an), dann hilft etwas Unix-Foo auf der Kommandozeile: Mangels einer entsprechenden Option für docker rm sammelt

    docker rm `docker ps -notrunc -a -q`

erst die IDs aller nichtlaufenden Container ein und schiebt diese dann zeilenweise dem Löschkommando unter.

Lerne Go und wirke mit

Unter Ubuntu 12.10/13.04/13.10 installiert sich der Docker-Service ganz einfach mit

    sudo apt-get install lxc-docker

Allerdings kommuniziert der Client mit dem nach einem Reboot des Rechners funktionsfähigen Docker-Dämon (Stand Version 0.8.1) über einen Root gehörenden Linux-Socket (/var/run/docker.sock), sodass der Anwender vor der Wahl steht, die Docker-Kommandos alle als Root abzusetzen oder alle Sicherheitsbedenken über Bord zu werfen und aller Welt Schreibrechte auf dem Socket einzuräumen. Langfristig sollte Docker vielleicht ausgefeiltere Eigentumsverhältnisse ermöglichen, was allerdings zugegebenermaßen nicht ganz trivial werden wird.

Insgesamt ist Docker wohl eines der derzeit heißesten Projekte auf Github, da die billige Pseudo-Virtualisierung deutliche Performancegewinne bei gleichzeitiger Komponenenten-Entkopplung verspricht. Wer mitwirken möchte, muss die neue Programmiersprache "Go" lernen, denn Docker ist darin abgefasst.

Infos

[1]

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

[2]

"Volle Ladung", Rob Knight, Linux-Magazin 08/2013

[3]

"Frischmacher", Perl-Skript hält den PC mit virtuellen Maschinen spurenfrei sauber, Michael Schilli, http://www.linux-magazin.de/Ausgaben/2011/10/Perl-Snapshot

[4]

Docker-Installation auf Ubuntu, http://docs.docker.io/en/latest/installation/ubuntulinux/

Michael Schilli

arbeitet als Software-Engineer bei Yahoo in Sunnyvale, 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.