Java: JBoss und WildFly Installationen parametrisieren

In der Regel gibt es für ein Projekt immer mehrere Installationen des Applicationservers, die möglichst auf dem gleichen Stand gehalten werden müssen:

  • Entwicklung, oft in der persönlichen/lokalen Entwicklungsumgebung jedes Entwicklers
  • Testumgebung bzw. Continuous Integration Umgebung
  • Produktion

Ein offensichtlicher Ansatz ist es, den Applicationserver auch per Git zu verwalten und Änderungen an einer Stelle zu "pushen" und auf allen anderen Systemen einen "Pull" auszuführen. Aber was ist mit den lokalen Konfigurationseinstellungen wie Datenbank-URL, Datenbank-User oder Emailserver-IP?

Es gibt die Möglichkeit, diese Parameter in eine lokale Property-Datei auszulagern, die nicht per Git verwaltet wird und damit in .gitignore aufgenommen werden muss. Wir nennen diese Datei z.B. system.properties (weil sie pro System erstellt wird). Hier ein Ausschnitt einer Property-Datei:

flows.db.driver: org.postgresql.Driver
flows.db.url: jdbc:postgresql://localhost/flows_mw
flows.db.user: mw  
smtp.server: 192.168.4.5        
          
Diese Properties lassen sich jetzt in den Konfigurationsdateien als Variablen verwenden, so dass beim JBoss beispielsweise die Datasource wie folgt konfiguriert wird:
<local-tx-datasource>
    <jndi-name>flows_db</jndi-name>
    <connection-url>${flows.db.url}</connection-url>
    <driver-class>${flows.db.driver}</driver-class>
    <user-name>${flows.db.user}</user-name>
...

Das geht aber noch weiter, denn Properties können auch im Quellcode der Anwendung verwendung finden, um z.B. Zugriffe auf Systeme zu parametrisieren, die in den Test- und Produktionsumgebungen unterschiedlich sind, wie Mailserver oder andere Backend-Systeme, mit denen kommuniziert wird.

Um den JBoss zu starten, wird die Properties-Datei einfach mit angegeben:

$JBOSS_HOME/bin/run.sh -P /home/flows/system.properties

Erfreulicherweise ist diese Vorgehensweise mit dem neuen WildFly 8 genauso möglich, hier am Beispiel des Standalone-Modus:

$JBOSS_HOME/bin/standalone.sh -P=/home/flows/system.properties

Mein Referenzprojekt dazu auf github: WildFly Instllationen mit Git verwalten

BuildID mit Gradle

Gerade in den Zeiten von DevOps, Continuous Integration und Continuous Delivery wird es immer wichtiger zu wissen, wer, wann ein Binary gebaut hat und auf welchem Stand des Quellcodes es beruht. Das klassische "projekt-yxz-1.2-snapshot" von Maven hilft hier nicht weiter, denn mit dieser Bezeichnung kann es beliebig viele Binaries geben, und einen Rückschluss auf den zu Grunde liegenden Commit in Git lässt es auch nicht zu. Die Frage kann sich auf dem CI-Server oder Testsystem stellen, um einen dort aufgetretenen Fehler zuzuordnen oder auch beim lokalen Entwickeln in kurzen Zyklen, wenn man sicher sein will, dass ein Deployment des neusten Builds wirklich stattgefunden hat.

Abhilfe schafft hier die BuildID, die immer eindeutig und automatisch bei jedem Build in das Binary kopiert wird. Dort kann es dann zur Laufzeit ausgelesen und z.B. im Footer einer Web-Anwendung oder im Info-Menüpunkt eines Rich-Clients angezeigt werden. Entscheidend ist der Automatismus: die BuildID wird immer während des Builds eindeutig vom Build-Script generiert, und kann daher nicht vergessen und muss nicht von Hand nachgezogen werden. Auch bei Releases nicht.

Mit einem Gradle Task ist das kein Problem.

task buildId << {   
    buildDir.mkdirs()
    // generate timestamp and user for build_id
    def build_id=new Date().format('yyyyMMdd HHmm ')+System.properties.'user.name'
    
    try {
        // get git status und ref
        def gitref="git rev-parse --short HEAD".execute().text.trim()
        def gittag="git describe --tags --always $gitref".execute().text.trim()
        //if(gitref!=gittag) { gitref="$gittag $gitref"}
        def gitdirty="git status --porcelain".execute().text.isEmpty()?" ":"*"
        build_id+=" $gittag$gitdirty"
    }
    catch (Exception x) {
        println "Warning: no git executable, using simple buildId"
    }
    println "buildId: $build_id"
    
    // and write build_id to file
    new File("$buildDir/build_id.txt").withWriter { out -> out.println build_id }
}
            
            
Das obige Beispiel erzeugt eine BuildID mit einem aktuellen Timestamp und dem Username. Dazu kommt noch der aktuelle Commit aus Git und mit dem Dirtyflag noch die Information, ob der Stand im Workspace verändert wurde (""uncommited changes"). Wenn es einen Release Tag gibt, wird statt des SHA1-Codes das Tag verwendet:
20140520 1622 mw V_1_2       // Gebaut von mw, Release Tag V_1_2, nicht dirty
20140521 1141 mw a5f734*     // Binary basiert auf Commit a5f734, dirty
            
Das Ganze funktioniert auch für Rollbacks, wenn z.B. im Release V_1_3 ein kritischer Bug aufgetreten ist und nun auf Release V_1_2 zurückgerollt werden soll. Die folgenden beiden Zeilen bauen das alte Release mit der passenden BuildID.
git checkout V_1_2
gradle clean war
Die BuildID kann dann von Gradle beim Build von Java Anwendungen in das META-INF Verzeichnis von JAR, WAR oder EAR Archiven kopiert werden:
war.dependsOn buildId
war {
    metaInf { 
        from "$buildDir/build_id.txt" 
    }
}
Und steht der Anwendung zur Laufzeit wie folgt zur Verfügung und kann z.B. im Footer angezeigt werden:
InputStream str = getClass().getResourceAsStream("/META-INF/build_id.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(str));
String buildId = br.readLine();
Mein Referenzprojekt dazu auf github: JTrack-EE7: RESTful Webservices, Gradle, Testautomatisierung mit Spock, JUnit und Arquillian

Linux KVM und DRBD: HA Virtualisierung, Kosten und Plattendurchsatz

Das Standard-Setup für hochverfügbare VMs besteht aus mehreren physikalischen Servern, die auf ein gemeinsames SAN zugreifen, auf den die Images der VMs liegen. Fällt ein physikalischer Server aus, werden die VMs auf einem anderen Server gestartet nachdem die zugehörigen LUNs auf dem SAN entweder automatisch oder von Hand dem neuen physikalischen Server zugeteilt wurden. Soweit so gut. Allerdings ist ein SAN mit HA-Eigenschaften oft teuer in Anschaffung und Wartung und muss dazu noch über wiederum teure und proprietäre Fiberchannel Swichtes angeschlossen werden. Hierfür Treiber und Support für Linux zu bekommen ist nicht einfach und oft ist diese Verbindung trotzdem ein Flaschenhals für den Durchsatz im Gesamtsystem.

Eine Alternative ist das Setup der physikalischen Server als Pärchen, die über das Distributed Replicated Block Device (DRBD) ihre Platten synchronisieren, also RAID über das Netz. Das Schöne ist, diese Kopplung kann direkt über ein dediziertes LAN-Kabel zwsichen den beiden Servern erfolgen, auch über 10GBit Interfaces ohne dass spezielle Treiber notwendig sind. Die physikalischen Server haben ihre Platten in dieser Lösung lokal über SAS angeschlossen, so dass die Latenzen trotz DRBD kleiner sind als bei der SAN-Lösung. Um viele Platten direkt an die physikalischen Server anzuschließen bietet sich ein JBOD an, damit hat man auch die freue Auswahl des Plattenherstellers. Beim Ausfall eines physikalischen Servers werden wie bei der SAN-Lösung auch, die VM auf auf dem zweiten Knoten wieder gestartet.

Hier noch einmal die Vorteile auf einen Blick:

  • Verwendung von Standard-Komponenten ohne Vendor-Lock-In
  • Keine proprietären Treiber notwendig: 10GBit LAN und DRBD sind im Linux Kernel vorhanden
  • Höherer Plattendurchsatz und kleinere Latenzen durch quasi-lokalen Plattenzugriff