19.7.2015 - Docker compose

Teil 1 Teil 2 Teil 3 Teil 4

Weiter geht’s mit Docker.

Wenn man sich erst mal an die ‘Docker way of life’ gewöhnt hat, wird man feststellen, dass man immer weniger Dienste direkt auf dem Server installiert. Stattdessen wird man für jeden Dienst einen Docker container verwenden, weil das eben viel einfacher in der Handhabung ist: Es gibt nie Kompatibilitätsprobleme, man muss nie lang über Konfigurationen nachdenken, und last but not least: Migration auf neue oder ganz andere Hardware ist trivial: Es ist einem Docker container schlicht egal, ob er auf Mac, Linux, Windows oder in der Amazon-ec2 cloud läuft. Er verhält sich immer genau gleich. Und es ist ihm auch egal, ob ein anderer Container, mit dem er kooperiert, sich auf demselben Computer oder auf einem anderen Kontinent befindet.

Doch einen Nachteil gibt es: Wenn man eine Menge Docker container laufen hat, muss man die bei einem Neustart des Servers alle korrekt wieder erstellen und/oder hochfahren, was manchmal schwierig sein kann, wenn das letzte Mal eine Weile her ist, und was auch eine Menge Tipparbeit an der Konsole bedeutet.

Hier kommt docker-compose ins Spiel. Damit kann man ein System aus beliebig vielen Containern mit einem simplen sudo docker-compose up & hochfahren und mit sudo docker-compose stop stoppen.

Dazu muss man sich einmal die Mühe machen, eine Steuerdatei (docker-compose.yml) zu erstellen. Die sieht zum Beispiel so aus:

    owncloud:
      image: rgwch/owncloud-client:latest
      volumes:
       - /srv/ebooks:/srv/owncloud
      env_file: ../ebenv.txt
    
    calibre:
       image: rgwch/calibre-server:latest
       volumes:
         - /srv/ebooks:/srv/calibre
       ports:
         - 4040:8080
    
    gitserver:
       build: docker-git
       volumes:
         - /srv/repositories:/opt/git
       ports:
         - 5050:80
    
    
    webelexis:
       build: docker-webelexis-server
       volumes:
         - /home/gerry/dockerfiles/cfglocal.json:/home/webelexis/cfglocal.json
         - /home/gerry/dockerfiles/wlxks.jks:/home/webelexis/ks.jks
       ports:
         - 443:2015
       environment:
         VERSION: 1.0.0

Das ist im Prinzip ja selbsterklärend. Hier werden zwei Container aus images und zwei aus build-instructions (Dockerfiles in den angegebenen Verzeichnissen) erstellt und anschliessend konfiguriert und hochgefahren. Anschliessend kann man mit http://<adresse>:4040 auf den eBook-Server, mit http://<adresse>:5050 auf den GIT-Server und mit https://<adresse> auf den Webelexis-Server zugreifen.

Docker-compose ist dabei auch schlau genug, um bereits existierende Container wiederzuverwenden, anstatt sie bei jedem Start neu zu erstellen. Neu erstellt wird nur, wenn es neuere Versionen gibt, oder wenn sich (im Fall von build:) Etwas an den build-instructions geändert hat.

Wenn man sudo docker-compose up ohne & am Schluss eingibt, dann bleibt der Prozess im Vordergrund und kann mit einmal CTRL-C sauber und mit zweimal CTRL_C “hart” gestoppt werden. Wenn man ihn mit & in den Hintergrund schickt, kann man ihn mit sudo docker-compose stop sauber und mit kill hart stoppen.

Wenn man alle Container der Komposition auf einmal löschen will, genügt ein simples sudo docker-compose rm. Man muss sich also nicht mehr mit jeder Menge angry_einsteins und condescent_newtons herumschlagen, um alle loszuwerden.

18.7.2015 - Docker zum Zweiten

Teil 1 Teil 2 Teil 3 Teil 4

Mehr von Docker

Jetzt wollen wir verschiedene Docker-Container miteinander spielen lassen:

sudo docker -d --env-file env.txt -v /srv/ebooks --name ebook-store rgwch/owncloud-client:latest
sudo docker -d -p 4040:8080 --volumes-from ebook-store rgwch/calibre-server:latest

Wenn env.txt so aussieht:

     URL=https://my.server.com/owncloud/remote.php/webdav/ebooks
     USER=ichselber
     PASSWORD=ganz_geheim

Dann wird dieses Arrangement folgendes tun: Es erstellt eine lokale Calibre-library in einem docker-data-container, hält diese Library synchron mit dem owncloud-Verzeichnis “ebooks” auf dem Server my.server.com und bietet diese Library auf port 4040 an. Mit irgendeinem kompatiblen eBook-reader (oder einem Web-Browser) kann man dann die Bibliothek durchsuchen und Bücher zum Lesen herunterladen.

Natürlich könnte man dasselbe auch ohne Docker erreichen. Was hier besticht, ist aber die Einfachheit. So ein System aufzusetzen dauert keine 10 Minuten. Und es läuft auf jedem Computer, auf dem Docker installiert ist.

Automated Builds

Wenn man ein wenig mit Docker herumspielt, stellt man bald fest, dass Docker images ein wenig unhandlich sind. Zwar tut Docker sein Bestes, um nur die Layers umherzuschieben, die wirklich unterschiedlich sind, aber vor allem der Upload eines selbstgemachten Images auf hub.docker.com ist doch eine arge Geduldsprobe. Und wenn man eine Kleinigkeit ändert, muss man erneut hochladen.

Man kann sich behelfen, indem man statt Docker Images nur die Dockerfiles umherschiebt und die Images auf jedem Computer separat mit docker build erstellt. Das kann aber je nach Image dann auch wieder ein recht zeitraubender Prozess sein. Aber es gibt einen einfacheren Weg, zumindest für OpenSource Dockerfiles:

  • github und docker accounts erstellen, wenn noch nicht vorhanden
  • Dockerfile und eventuell für den build benötigte Zusatzdateien in ein öffentliches Github Repository stellen
  • Auf hub.docker.com Add Repository und dort automated build auswählen. Dann GitHub wählen und das vorhin erstellte Repository aufsuchen.
  • Auf GitHub das Repository aufsuchen, dort auf Settings gehen, webhooks&services anklicken, unter services nach “Docker” suchen und aktivieren.

Von jetzt an wird jedes Mal, wenn ein Push ins GitHub Repository erfolgt, ein neues Docker-Image erstellt und automatisch auf Dockerhub bereitgestellt. Das Readme.md des Github Repositories wird dann zur Description des Docker Images.

13.7.2015 - Git Error 411

Git meldet beim Push folgendes:

$> git push origin master
Counting objects: 117, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (103/103), done.
error: RPC failed; result=22, HTTP code = 411
fatal: The remote end hung up unexpectedly

Tja, was nun?

Wenn man Zugriff auf den Server hat, kann man das Server log anschauen:

2015-07-13 15:21:44: (request.c.1125) POST-request, but content-length missing -> 411 

Tja, was nun?

Google hilft, aber nur mit den exakt richtigen Suchbegriffen…

Kurz: Folgendes muss man beim git repository eingeben:

`git config http.postBuffer 524288000`

Dann klappt’s auch mit dem Pushen.

5.7.2015 - Maven - Plugin! Plug in!

Teil 1 Teil 2 Teil 3 Teil 4

Plug in!

Irgendwann will man etwas, was Maven nicht kann. Dann macht man sich entweder auf die Suche nach einem Plugin mit den gewünschten Fähigkeiten, oder man schreibt selber eins. Das ist verblüffenderweise gar nicht so schwierig.

Anstatt irgendein Pseudoprojekt zu erstellen, beziehe ich mich im Folgenden auf ein real existierendes Projekt: Webelexis, und ein real existierendes Plugin: mimosa-maven-plugin, das ich für dieses Projekt gemacht habe.

Die Idee ist folgende: Das Teilprojekt webelexis-client ist eine JavaScript-Applikation, das Teilprojekt webelexis-server ist ein Java-Programm. Das Gesamtprojekt besteht dann aus dem Server, welcher das client-Programm in seinem Ubterverzeichnis ‘web’ hält, und mit diesem zusammen in ein einziges jar gepackt wird, welches der Anwender dann einfach mit java -jar webelexis-servr-x.y.z.jar starten kann und sofort eine voll funktionsfähige Web-Anwendung hat. Um das zu erreichen sind folgende Arbeitsschritte notwendig:

Clientseitig:

  • Jade-Dateien nach HTML compilieren
  • JavaScript Dateien minimieren und zusammenfassen
  • CSS Dateien minimieren und zusammenfassen
  • alles andere unverändert ins Zielverzeichnis kopieren
  • Unit-Tests laufen lassen

Diese ganzen clientseitigen Arbeiten habe ich mimosa übertragen. Ein einfaches mimosa build -m baut die minimierte Version der Website im Verzeichnis webelexis-client/dist

Serverseitig

  • Java Dateien compilieren
  • Ressourcen ins Zielverzeichnis kopieren
  • Tests laufen lassen
  • Alles in ein .jar packen.

Diese Aufgaben erledigt, wen wundert’s, Maven.

Bisher:

Der manuelle Ablauf ist so, dass man zuerst mimosa build im client startet, dann maven compile im server, dann den Inhalt von “webelexis-client/dist” nach “webelexis-server/target/classes/web” kopiert und dann maven package aufruft, um alles zusammenzupacken.

Neu:

Okay, man könnte sich einfach ein Shell-Script schreiben, was das alles erledigt. Oder meinetwegen einen Ant-Task. Oder man könnte ein existierendes Maven-Plugin, zum Beispiel exec-maven-plugin einsetzen. Aber wir wollen ja etwas lernen. Also bauen wir unser eigenes Plugin für diesen Job. Das Ziel ist: Ich gebe nur maven package ein, und dann werden client und server gebaut, getestet, zusammenkopiert und in ein jar abgefüllt.

Aufbau:

Das Plugin besteht aus folgenden Teilen:

pom.xml (wer hätte das gedacht?) Ich zitiere hier nur die relevanten Stellen. Die ganze Datei kann im Quelltext mit oben im ersten Absatz stehendem Link heruntergeladen werden.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>rgwch</groupId>
    <artifactId>mimosa-maven-plugin</artifactId>
    <version>1.0.1</version>
    <packaging>maven-plugin</packaging>
    <dependencies>
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-plugin-api</artifactId>
            <version>3.3.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven.plugin-tools</groupId>
            <artifactId>maven-plugin-annotations</artifactId>
            <version>3.4</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-io</artifactId>
            <version>1.3.2</version>
        </dependency>

    </dependencies>
</project>

Das Einzige, was anders ist, als bei anderen Maven-Projekten, ist <packaging>

Und dann im Wesentlichen der Klasse ch.rgw.mmp.MimosaMavenPlugin.java:

package ch.rgw.mmp;

import org.apache.commons.io.FileUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;

import java.io.File;
import java.io.IOException;

@Mojo(name = "mimosa")
public class MimosaMavenPlugin extends AbstractMojo {

    @Parameter private File source;
    @Parameter private File intermediate;
    @Parameter private File dest;
    @Parameter private String mimosaOptions;

    public void execute() throws MojoExecutionException, MojoFailureException {
        if (!dest.exists() && !dest.mkdirs()) {
        throw new MojoFailureException(null, "could not create directory " + dest.getAbsolutePath(), "");
        }
        getLog().info("launching mimosa from " + source.getAbsolutePath());
        try {
        Process mimosa=Runtime.getRuntime().exec("mimosa build", null,source);
        StreamBuffer errorStream=new StreamBuffer(mimosa.getErrorStream(),"ERROR");
        StreamBuffer outputStream=new StreamBuffer(mimosa.getInputStream(),"OUTPUT");
        errorStream.start();
        outputStream.start();
        getLog().info(Integer.toString(mimosa.waitFor()));
        FileUtils.copyDirectory(intermediate,dest);
        } catch (IOException e) {
        e.printStackTrace();
        throw new MojoFailureException(e.getMessage());
        } catch (InterruptedException e) {
        e.printStackTrace();
        throw new MojoFailureException(e.getMessage());
        }
    }
}

Das ist alles. Die Klasse StreamBuffer liste ich hier jetzt nicht auf, die erledigt nur das Handling des outputs von Mimosa. Die einzige Besonderheit dieser Klasse sind die Annotationen @Mojo und @Parameter, sowie der Aufruf von getLog(). Alles Andere ist standard Java.

@Mojo, das mich immer an diese kanarische Sauce erinnert, ist das Kommando mit dem dieses Plugin involviert wird. Hier definieren wir, dass es auf den Namen “mimosa” hören soll.

@Parameter sind die Werte, die das aufrufende Projekt dem Plugin mitgeben kann.

Einsatz

Der Aufruf unseres Plugins in der pom.xml von webelexis-server sieht so aus:

<plugin>
    <groupId>rgwch</groupId>
    <artifactId>mimosa-maven-plugin</artifactId>
    <version>1.0.1</version>
    <configuration>
        <source>../webelexis-client</source>
        <intermediate>../webelexis-client/dist</intermediate>
        <dest>target/classes/web</dest>
        <mimosaOptions>-m</mimosaOptions>
    </configuration>
    <executions>
        <execution>
            <id>1</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>mimosa</goal></goals>
        </execution>
    </executions>
</plugin>

Hier wird deklariert, welche Parameter das Plugin bekommt, nämlich das Basisverzeichnis des Client-Projekts, dessen Ausgabeverzeichnis, sowie das endgültige Zielverzeichnis im Server-Projekt. Dann wird unter <executions> festgelegt, wann es ausgeführt wird, und welches Kommando ausgeführt wird: Wir möchten, dass in der Phase “prepare-package” das Kommando “mimosa” unseres Plugins ausgeführt wird. Danach soll der Inhalt des mimosa-Ausgabeverzeichnisses ins Unterverzeichnis “web” des server-Ausgabeverzeichnisses kopiert werden.

Das ist eigentlich alles. Wenn wir jetzt zunächst im Verzeichnis mimosa-maven-plugin den Befehl mvn install ausführen, können wir anschliessend im Server-Verzeichnis mvn package aufrufen und erhalten am Ende ein .jar, welches den verarbeiteten Server- und Client- Code enthält.

Publish or perish

Um ein Plugin auch anderen zur Verfügung zu stellen, muss man ein wenig mehr Aufwand betreiben. Bisher haben wir es ja nur mittels mvn installim lokalen Repository installiert.

Wir könnten es z.B. nach maven central schicken, oder an einen anderen vom Internet aus erreichbaren Ort. Im Prinzip spielt das keine Rolle. Ich zeige hier das Vorgehen für Bintray. Bintray ist ein Hoster für Binärdateien von OpenSource Projekten. Die Firma ist sehr liberal. Ausser einem kostenlosen Konto gibt es keine Voraussetzungen. Mit einem solchen kostenlosen Konto hat man automatisch ein Maven-Repository (und zusätzlich auch Repositories für rpm, debian, docker und Anderes).

Nach dem Erstellen eines Kontos geht es los:

Package erstellen:

  • Maven -> Add new Package

  • Name: mimosa-maven-plugin (Der Name ist nicht ganz zufällig gewählt, sondern wird von Apache so vorgeschlagen. Auf keinen Fall sollte man sein Plugin etwa “maven-mimosa-plugin” nennen, da alle Namen beginnend mit “Maven” für Apache reserviert sind. Selbstverständlich beachten wir dieses Anliegen.)

  • Licenses: Hier muss man mindestens eine OpenSource Lizenz eingeben. Das ist Bedingung für das kostenlose Hosten auf Bintray

  • Version Control: Hier muss man angeben, wo der Quellcode zu finden ist. In unserem Fall ist das http://gitlab.com/rgwch/mimosa-maven-plugin.

Alle anderen Felder sind nicht zwingend.

Mit “Create Package” wird die Package dann erstellt.

Version erstellen

Das eben erstellte mimosa-maven-plugin package anwählen und dort “new version” eingeben. Idealerweise gibt man hier dasselbe ein, was man auch in der pom.xml eingetragen hat (das wird zar nicht überprüft, erscheint aber vernünftig).

Dateien hochladen

Die eben erstellte Version anklicken und “upload file” wählen. Wichtig ist nun das Feld Target repository path. Hier muss der maven-konforme Pfad (also so wie im lokalen maven repository) eingegeben werden. In unserem Fall ist das: rgwch/mimosa-maven-plugin/1.0.1

Dann muss man mindestens zwei Dateien hochladen:

  • mimosa-maven-plugin-1.0.1.jar
  • mimosa-maven-plugin-1.0.1.pom

Das zweite ist die pom.xml, die vor dem Hochladen so umbenannt werden muss.

Nachdem wir für beide Dateien “publish” freigegeben haben, sind sie vom Internet aus erreichbar. Allerdings wird Maven sie nicht finden. Wie sollte es auch? “irgendwo im Internet” ist keine ausreichend genaue Beschreibung.

pom.xml anpassen

Folgender Eintrag in der pom.xml von webelexis-server löst dieses Problem:

<pluginRepositories>
    <pluginRepository>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
        <id>bintray-rgwch-maven</id>
        <name>bintray-plugins</name>
        <url>http://dl.bintray.com/rgwch/maven</url>
    </pluginRepository>
    <pluginRepository>
        <releases>
            <updatePolicy>never</updatePolicy>
        </releases>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
        <id>central</id>
        <name>Central Repository</name>
        <url>http://repo.maven.apache.org/maven2</url>
    </pluginRepository>
</pluginRepositories>

Damit erklären wir Maven, dass es Plugins nicht nur in repo.maven.apache.org, sondern auch in dl.bintray.com/rgwch/maven suchen soll.

Jetzt kann jeder nodejs- und maven-Besitzer Webelexis sehr einfach selber bauen:

git clone https://github.com/rgwch/webelexis.git
sudo npm install -g mimosa
cd webelexis/webelexis-server
mvn package

Das ist alles. Das mimosa-maven-plugin wird im Rahmen von mvn package automatisch von bintray heruntergeladen, installiert und ausgeführt.

RTFM

Ohne Dokumentation werden die Anwender unseres neuen Plugins viel zuviele Rückfragen stellen. Und am liebsten wollen sie die Dokumentation so, wie sie sie gewohnt sind. Zum Beispiel in derselben Form, wie Libraries und Plugins in maven central dokumentiert sind.

Nicht leichter als das.

mvn site 

erledigt das und erstellt ein Verzeichnins “site” in mimosa-maven-plugin/target, das man unverändert auf einen Webserver kopieren kann. Das Resultat sieht dann so aus.