DockerDuke

Docker und Java – Teil 3: Der Build-Prozess

Im zweiten Teil der Reihe wurden die notwendigen Werkzeuge vorgestellt und installiert. Nun werden wir den Build-Prozess für Java-Projekte und die Erstellung der Docker-Images zusammenführen.

Teil 3: Der Build-Prozess

Build von Java-Projekten mit Apache Maven

Apache Maven ist neben Apache Ant mit Ivy und Gradle ein Standard-Werkzeug, das zum automatisierten Erzeugen von Java-Archiven benutzt werden kann. Allen Werkzeugen gemeinsam ist das Dependency Management: Projekt-Abhängigkeiten werden in einer Konfigurationsdatei abgelegt und vom Build-Werkzeug automatisch aus einem Artefakt-Repository geladen. Repositories werden von verschiedenen Anbietern frei zugänglich im Internet betrieben. Maven Central ist das wahrscheinlich bekannteste Beispiel.

Um die selbst erzeugten Artefakte selbst verwalten zu können, werden im Unternehmen meistens eigene Server betrieben. Dazu existieren sofort einsetzbare Produkte wie Sonatype NexusJFrog Artefactory oder Apache Archiva.

Entscheiden wir uns für einen Maven-basierten Build-Prozess erfolgt die allgemeine Konfiguration des Build-Prozesses mit zwei Dateien:

  • Die settings.xml enthält die URL des Artefakt-Repositories und gegebenenfalls Authentifizierungs-Informationen
<settings>
	<mirrors>
		<mirror>
			<id>nexus</id>
			<mirrorOf>*</mirrorOf>
			<url>http://xxxxx/repository/maven-public/</url>
		</mirror>
	</mirrors>
	<repositories>
		<repository>
			<id>central</id>
			<url>http://central</url>
			<releases><enabled>true</enabled></releases>
			<snapshots><enabled>true</enabled></snapshots>
		</repository>
	</repositories>
	<pluginRepositories>
		<pluginRepository>
			<id>central</id>
			<url>http://central</url>
			<releases><enabled>true</enabled></releases>
			<snapshots><enabled>true</enabled></snapshots>
		</pluginRepository>
	</pluginRepositories>
	<servers>
		<server>
			<id>nexus</id>
			<username>xxx</username>
			<password>xxx</password>
	</server>
	</servers>
</settings>

 

Ein Parent-POM enthält neben allgemeinen Informationen für alle Build-Prozesse auch die Informationen zum Ausbringen der Artefakte in das Repository

<distributionManagement>
<repository>
<uniqueVersion>false</uniqueVersion>
<id>nexus</id>
<name>Corporate Repository</name>
<url>http://…</url>
</repository>
<snapshotRepository>
<uniqueVersion>true</uniqueVersion>
<id>nexus</id>
<name>Corporate Snapshots</name>
<url>http://…</url>
</snapshotRepository>
</distributionManagement>

 

Nach all diesen Vorbereitungen ist der Build-Prozess eines eigenen Projekts sehr einfach: Es wird ein Projekt-POM definiert, das im einfachsten Fall nur den Parent angeben muss.

<project>
<modelVersion>4.0.0</modelVersion>
<groupId>org.javacream.training</groupId>
<artifactId>org.javacream.training.business</artifactId>
<version>1.0</version>
<parent>
<groupId>org.javacream.training.docker</groupId>
<artifactId>org.javacream.training.docker.parent</artifactId>
<version>1.0</version>
</parent>
</project>

 

Trotz dieser wirklich sehr einfachen Konfiguration kann das Java-Projekt sofort bis hin zu verschiedenen Phasen gebaut werden:

  • compile: Das Projekt wird kompiliert
  • package: Ein Java-Archiv wird im target-Verzeichnis des Projekts erzeugt
  • deploy: Das Archiv wird in das Unternehmens-Repository ausgebracht

So einfach funktioniert Maven!

Ein Maven System
Ein Maven System

Maven und Docker

Auch Docker definiert einen Build-Prozess: Images werden aus dem Dockerfile erzeugt. Was liegt also näher, als diesen Prozess mit Maven zu integrieren? Diese Idee wird durch Maven-PlugIns für Docker umgesetzt. Hierzu stehen sogar verschieden Produkte zur Auswahl:

 

  • Durch das Parent-POM ist die Integration dieser PlugIns für den Entwickler vollkommen transparent! Wird beispielsweise das Spotify-PlugIn in den Parent aufgenommen

 

<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.4.11</version>
<configuration>
<imageName>${docker.namespace.prefix}/${project.artifactId}</imageName>
<dockerDirectory>src/main/docker</dockerDirectory>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>libs/*.jar</include>
</resource>
</resources>
<imageTags>
<imageTag>${project.version}</imageTag>
<imageTag>latest</imageTag>
</imageTags>
</configuration>
</plugin>

 

so stehen nun neue Maven-Befehle zur Verfügung, insbesondere

  • docker:build: Erzeugen des Images aus dem Dockerfile des Projekts
  • docker:push: Pushen des Images in ein Docker-Repository

Damit ist die gewünschte Integration der Build-Prozesse durchgeführt. Und nachdem Nexus oder Artefactory neben Java-Artefakten auch Docker-Images verwalten können, ist unser System praktisch fertig! Dass die Quellcodes, das POM und das Dockerfile in einem Versionsverwaltungssystem abgelegt werden und auf ein Build-Server wie Jenkins die Builds automatisiert ablaufen lassen wird, ist selbstverständlich.

Die vollständige Parent-POM kann hier angezeigt werden:

<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>org.javacream.training.docker</groupId>
	<artifactId>org.javacream.training.docker.parent</artifactId>
	<version>1.0</version>
	<packaging>pom</packaging>
	<properties>
		<docker.namespace.prefix>javacream</docker.namespace.prefix>
	</properties>
	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.12</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-core</artifactId>
			<version>2.8</version>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-dependency-plugin</artifactId>
				<version>3.0.0</version>
				<executions>
					<execution>
						<id>copy-dependencies</id>
						<phase>package</phase>
						<goals>
							<goal>copy-dependencies</goal>
						</goals>
						<configuration>
							<outputDirectory>${project.build.directory}/libs</outputDirectory>
							<overWriteReleases>false</overWriteReleases>
							<overWriteSnapshots>false</overWriteSnapshots>
							<overWriteIfNewer>true</overWriteIfNewer>
						</configuration>
					</execution>
				</executions>
			</plugin>

			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.6.1</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>

			<plugin>
				<groupId>com.spotify</groupId>
				<artifactId>docker-maven-plugin</artifactId>
				<version>0.4.11</version>
				<configuration>
					<imageName>${docker.namespace.prefix}/${project.artifactId}</imageName>
					<dockerDirectory>src/main/docker</dockerDirectory>
					<resources>
						<resource>
							<targetPath>/</targetPath>
							<directory>${project.build.directory}</directory>
							<include>${project.build.finalName}.jar</include>
						</resource>
						<resource>
							<targetPath>/</targetPath>
							<directory>${project.build.directory}</directory>
							<include>libs/*.jar</include>
						</resource>
					</resources>
					<imageTags>
						<imageTag>${project.version}</imageTag>
						<imageTag>latest</imageTag>
						<imageTag>localhost:5000/${project.build.finalName}</imageTag>
					</imageTags>
				</configuration>
			</plugin>
		</plugins>
	</build>
	<distributionManagement>
		<repository>
			<uniqueVersion>false</uniqueVersion>
			<id>nexus</id>
			<name>Corporate Repository</name>
			<url>http://10.44.1.101:8081/repository/maven-releases/</url>
			<layout>default</layout>
		</repository>
		<snapshotRepository>
			<uniqueVersion>true</uniqueVersion>
			<id>nexus</id>
			<name>Corporate Snapshots</name>
			<url>http://10.44.1.101:8081/repository/maven-snapshots/</url>
			<layout>legacy</layout>
		</snapshotRepository>
	</distributionManagement>
</project>

 


Im vierten und letzten Teil wird dann programmiert: Wir erstellen einen Microservice auf Basis von Spring Boot!


Seminare zum Thema

 

Weiterlesen

DockerDuke

Docker und Java Teil 2 – Installation und erstes Arbeiten

Nachdem wir uns im ersten Teil der Reihe um Docker gekümmert haben geht es im zweiten Teil um die Installation der Werkzeuge, die für ein effizientes Arbeiten mit Docker und Java notwendig sind.

Teil 2: Installation und erstes Arbeiten

SUSE Linux Enterprise

Schritte

Die Installation von Docker auf verschiedenen Plattformen ist in der Dokumentation sehr gut beschrieben. Dabei ist auf Linux-Systemen wie SUSE Enterprise diese Routine sehr einfach durchzuführen:

  1. Hinzufügen des Docker-Repositories mit sudo zypper addrepo https://yum.dockerproject.org/repo/main/opensuse/13.2/ docker-main
  2. Refresh des SUSE Package Managers mit sudo zypper refresh
  3. Die eigentliche Installation erfolgt mit sudo zypper install docker-engine

Während der Installation werden eventuell einige Warnungen bezüglich Signaturen etc. angezeigt, die aber einfach akzeptiert werden können.

Docker ist hiermit installiert, wie einfach überprüft werden kann:

docker -v
mit der Ausgabe
Docker version 1.13.1, build 092cba3

Andererseits scheitern alle anderen docker-Befehle an fehlenden Berechtigungen. Dies liegt aber nur daran, dass der Docker-Dämon als root-Benutzer ausgeführt wird. Zur Lösung gibt es zwei Möglichkeiten:

  • Arbeiten mit sudo. Dies ist ziemlich “quick & dirty” und auf Dauer nicht genügend.
  • Hinzufügen von Usern zur Gruppe docker. Dies ist die saubere Lösung, die jetzt beschrieben wird:
    1. Erstellen der Gruppe docker mit sudo groupadd docker. Allerdings ist diese  Gruppe wahrscheinlich bereits bei der Installation angelegt worden.
    2. Anschließend wird der User  mit sudo gpasswd -a ${USERNAME} docker hinzugefügt.

Nach einer Neuanmeldung am System ist die Installation komplett. Der Start des Dämons erfolgt mit: sudo service docker start.

Was ist während der Installation alles passiert?

  • Es wurde der Docker-Dämon installiert und gestartet. Dieser verwaltet alle Images und Container. Außerdem ist er in der Lage, über https mit dem Docker-Hub-Repository zu kommunizieren.
  • Es wurde ein lokales Repository für Images und Container angelegt. Dieses befindet sich in /var/lib/docker
  • Der Docker-Client ist ein Konsolen-Programm, mit dessen Hilfe an den Docker-Dämon Befehle gesendet werden können. Dieser Client ist für einen Benutzer das primäre Werkzeug.
Die Bestandteile von Docker im Zusammenspiel

Kurzübersicht wichtiger Docker-Kommandos

Der Befehlssatz des Docker-Clients ist umfangreich, eine detaillierte Beschreibung ist in der Referenz-Dokumentation aufgeführt. Wichtige Befehle sind:

  • pull: Lädt ein Image vom entfernten Repository
  • create: Erzeugt einen Container
  • start|stop: Start/Stop eines Containers
  • run: Erzeugt einen Container und startet diesen sofort
  • build: Erstellt ein Image

Eclipse und das Docker-PlugIn

Eclipse wird für SUSE Linux  als Archiv zur Verfügung gestellt. Nach dem Download wird dieses anschließend extrahiert. Dann wird das Docker-PlugIn installiert. Damit stehen schließlich innerhalb der Java-Entwicklungsumgebung Views zur Benutzung von Docker-Befehlen zur Verfügung. Oder anders formuliert: Ein Java-Entwickler benutzt das PlugIn mit Dialogen und Kontextmenüs statt des Konsolen-basierten Docker-Clients. Weiterhin existiert ein eigener Editor, der das Format eines Dockerfiles unterstützt. Und schließlich werden in einer Übersicht alle vorhandenen Images und Container angezeigt.

Nach dem Öffnen der Docker Tooling Perspektive findet das PlugIn automatisch eine Verbindung zum Docker-Dämon auf dem lokalen Rechner. Sollte dies nicht der Fall sein, so ist wahrscheinlich der aktuelle Benutzer nicht Bestandteil der docker-Gruppe und hat folglich keine Zugriffs-Berechtigung. In diesem Fall prüfen Sie bitte Ihre Installation und melden sich anschließend nochmals neu an.

Erstes Arbeiten

Jetzt ist die Bühne bereitet, um ein erstes eigenes Java-Image zu erstellen und auszuführen!

Dazu erzeugen wir im Dockerfile-Editor die folgende Textdatei namens Dockerfile:

FROM openjdk:latest
ENTRYPOINT java -version

Ein Rechtsklick auf das Dockerfile öffnet das Kontextmenü:

Run As - Docker Image Build

Die Ausgaben zeigen zwei Schritte:

  1. Erstens wird das  openjdk-Layer geladen. Dies kann etwas dauern, da es einmalig von Docker-Hub geladen werden muss.
  2. Zweitens wird der EntryPoint hinzugefügt. Dies erzeugt faktisch ein weiteres Layer.

Entry Point

Genauso hätte das Erzeugen des Images auch über den Befehl
docker build -t javacream
erfolgen können.

Was haben wir nun alles im lokalen Repository? Das zeigt die Explorer-View:

 Explorer-View lokalen Repository

Neben unserem eben erzeugten Image wurde somit automatisch das in der FROM-Klausel angegebenen Basis-Image geladen.

Zum Abschluss erzeugen wir noch einen temporären Container, der das Image als Vorlage hat und anschließend gestartet wird. Dies erreichen wir, indem im Kontextmenü des Images Run... aufgerufen wird:

Temporärer Container mit Image als Vorlage

Mit der Ausgabe:

Log for Container

Auch hier hätte ein Kommando den gleichen Effekt gehabt, beispielsweise

docker run --rm javacream


Im nächsten Teil des Artikels geht es um das Build-Management. Wie kann das Erzeugen der Docker-Images automatisiert werden? Und wie werden Images über ein Repository zur Verfügung gestellt?


Seminare zum Thema

Konferenzen zum Thema

  • DevOpsCon
  • Die Konferenz für Continuous Delivery, Microservices, Docker, Clouds & Lean Business

Weiterlesen

DockerDuke

Docker und Java Teil 1 – Einführung in Docker

In diesem mehrteiligen Artikel möchte ich in einem pragmatischen Ansatz Docker und dessen Einsatz in Java-Projekten vorstellen.

Teil 1: Was ist eigentlich Docker?

Architektur

Docker ist eine Container-Lösung, die für eigene Anwendungen eine gekapselte und konfigurierbare Laufzeitumgebung zur Verfügung stellt.

Applications versus Container
Applikationen versus Container

Im Gegensatz zu einer Virtualisierungssoftware wie VMware Player oder Virtual Box sind diese Container jedoch leichtgewichtig, da sie kein komplettes Betriebssystem emulieren. Sie werden statt dessen “nur” in abgeschotteten Bereichen auf einem Host-System ausgeführt.

Layers und Images

Docker-Images werden durch so genannte “Layer” komponiert, wobei jedes Layer dem Image eine bestimmte Funktionalität oder Ressource (z.B. das Dateisystem oder Netzwerk) hinzufügt. Nachdem auf diese Art und Weise eine Basis geschaffen wurde, werden auf dem Image weitere Programme installiert und konfiguriert. Auf diese Art und Weise wird das Basis-Image modifiziert und dadurch effektiv um weitere Layers erweitert.

Hier als Beispiel die einzelnen Layers eines Images für einen MySQL-Datenbankserver:

MySQL-Image
MySQL-Image, visualisiert von MicroBadger

Beachten Sie hier insbesondere die Größe des Images und wie viel die einzelnen Layer dazu beitragen.

Images werden intern durch einen Hash-Wert eindeutig identifiziert. Dies ermöglicht folglich eine sehr effiziente Verwaltung: Haben zwei Images den gleichen Hash-Wert, so müssen diese zwangsläufig identisch sein. Deshalb können Images zentral in einem Repository verwaltet werden! Somit kann eine Docker-Installation auf dem Zielsystem einen lokalen Cache einrichten. Und zentrale Repositories sind im Internet verfügbar, als Paradebeispiel Docker Hub. Das Angebot ist übrigens sehr umfassend: Linux-Betriebssysteme, Datenbanken, Server-Lösungen…, ein Erkunden ist sehr zu empfehlen!

Die Installation eines Images auf einem eigenen Rechner ist brilliant einfach: Sie benötigen z.B. eine MySQL-Datenbank?

docker pull mysql

Das war es bereits!

Container

Docker-Container werden aus einem Image heraus erzeugt. Auch der Container selbst wird über einen Hash-Wert identifiziert. Folglich kann er in einem Cache auf dem Host-Dateisystem abgelegt werden. Der Container selber wird anschließend auf dem Host ausgeführt.

Beim Erzeugen des Containers aus einem Image können verschiedene Konfigurationen durchgeführt werden:

  • Port-Mappings: Enthält das Image beispielsweise einen Datenbank-Server, der auf dem Port 3306 lauscht, so kann der Host diesen Port auf 4306 bereitstellen.
  • Mounten von Dateisystemen: Ein beliebiges Verzeichnis des Hosts kann in den Container gemounted werden. Auch hier ist eine Datenbank ein gutes Beispiel: So kann beispielsweise das Log-Verzeichnis im Host-Dateisystem liegen. Nicht vergessen: Das Dateisystem des Containers ist komplett abgeschottet und deshalb sonst nicht zugreifbar!
  • Setzen von Environment-Variablen: Das Image definiert einen beliebigen Satz von Keys, die beim Erzeugen des Containers mit Values vorbelegt werden können. Welche Keys benutzt werden hängt natürlich vom Image ab. So könnte ein Datenbank-Container das Root-Password setzen lassen.

Auch hier wieder ein konkretes Beispiel: Wir erzeugen den MySQL-Container und starten diesen anschließend:

docker run -p 4306:3306 -v /my_log:/var/log/mysql -e MYSQL_ROOT_PASSWORD=pw mysql

Und schon läuft eine MySQL-Datenbank auf dem Port 4306 und verwendet intern das angegebene Root-Passwort. Als Log-Verzeichnis benutzt der Container das Verzeichnis /my_log des Hosts.


Im nächsten Teil des Artikels werden wir uns dann näher mit der Installation der Werkzeuge beschäftigen. Außerdem werden wir ein erstes eigenes Docker-Image erzeugen.


Seminare zum Thema

 

Weiterlesen

Mobile und Internet of Things

Mobile und Internet of Things: Eine Java-Vision wird Realität

“Mobile” und besonders “Internet of Things” sind wesentliche Bestandteile der strategischen Ausrichtung einer mordernen IT-Architektur. Allerdings wurde bereits in der ersten Beta-Version der Programmiersprache Java 1995 eine Vision formuliert, die eine ähnliche Ausrichtung hatte: “Write once, run anywhere”. In den folgenden Jahren etablierte sich Java jedoch hauptsächlich als Sprache für Server-Anwendungen. 

Allerdings lässt die aktuelle Zahl von Java-Installationen weltweit aufhorchen. Heute ist Java weltweit bereits auf mehr als 15 Milliarden Geräten installiert. Für das Jahr 2020 werden etwa 25 bis 50 Milliarden Java-Devices prognostiziert. Wie ist das möglich?

  • Die Anzahl von Server-Maschinen weltweit erreicht bei weitem nicht diese Höhe.
  • Java ist zwar auch auf dem Desktop durchaus präsent, hat sich aber gegenüber nativen Applikationen und Browser-basierten Anwendungen nicht wirklich durchsetzen können. Außerdem schrumpft dieser Markt tendenziell.

Mobile Java

Die Erklärung für diese großen Zahlen finden wir an ganz anderer Stelle:

  • Einerseits im Markt für mobile Endgeräte, also Smartphones und Tablets. So setzt Google mit Android auf Java-basierte Apps. Weitere Zuwächse versprechen Wearables wie Smartwatches.
  • Geräte der Unterhaltungselektronik wie TV-Boxen und Media-Player werden häufig mit Android ausgeliefert. Auch auf jedem Blu-Ray-Player läuft Java.
  • Den größten Markt für Java-Endgeräte stellen jedoch Smart Cards und Embedded Devices dar! Zusammen mit den Herstellern hat Oracle hierfür Java Embedded, die Micro Edition und Java Card eingeführt. Damit entwickeln und installieren Firmen bereits seit Jahren individuell angepasste Software auf SIM- und Kreditkarten.

Internet of Things

Mit dem “Internet of Things” wird Java noch wichtiger. Kleingeräte werden dafür sorgen, dass praktisch alle „Dinge“ – egal ob Körpersensor, Küchenmaschine, Automotor oder Hauselektronik – Daten erfassen und Aktionen ferngesteuert ausführen können. Und das mit einer Technologie, die seit vielen Jahren etabliert, ausgereift und sofort einsetzbar ist: Java.

Integrata-Seminare zum Thema:

Entwickeln von Android Apps
Entwickeln von Android Apps für Java-Entwickler

Bildnachweis: pixabay CC0 Public Domain by jeferrb

Weiterlesen

Lambda Ausdrücke

Web Development – Ein Java-Klassiker bleibt aktuell

Web Development bleibt auch in den Zeiten der App-Programmierung ein aktuelles Thema. Ursprünglich wurden Browser-Anwendungen nach dem MVC2-Pattern unter Benutzung einer serverseitigen View konzipiert. Heutzutage überwiegen jedoch Single Page-Applikationen. Diese nutzen asynchrone AJAX-Aufrufe unter konsequenter Verwendung einer RESTful Architektur. Das führte zu einer Vielzahl an neuen Frameworks und Sprachen. Doch trotz des grundsätzlichen Wandels sind beide Herangehensweisen weiterhin im Spiel.

MVC2 versus REST

MVC2-Anwendungen werden mit JavaServer Faces, einem Bestandteil der Java Enterprise Edition implementiert. Aber auch das proprietären Spring MVC-Framework ist immer noch sehr beliebt. Beide Frameworks sind ausgereift und bieten ein einfaches Programmiermodell mit mächtigem Funktionsumfang.

RESTful Server werden mit JAX-RS programmiert. Diese Bibliothek ist zwar deutlich jünger als JSF, aber auch wesentlich schlanker, da REST bereits durch den http-Standard umgesetzt ist.

Synergien zwischen Java und JavaScript

In Konkurrenz zu JavaScript-Frameworks wie jQuery, Angular, Backbone und React, um nur einige der Neuentwicklungen zu nennen, verlor Java seine Führungsposition im Web Development. Mit Hilfe eines Tricks können Browser-Anwendungen dennoch auch weiterhin mit Java programmiert werden: Ein Source-to-Source-Compiler transcompiliert bzw. transpiliert Java-Logik nach JavaScript. JavaServer Faces definiert AJAX-Aufrufe deklarativ als Bestandteil der Seitendefinition − eine direkte Browser-Programmierung ist unnötig.

Frameworks wie Google Web Toolkit (GWT) oder Vaadin definieren die Client-Oberfläche und den RESTful Server mit Java-Programmen. Der Client-Code wird direkt nach JavaScript transpiliert. Eclipse RCP- und JavaFX-Anwendungen können nach moderaten Anpassungen im Browser ausgeführt werden. Mit ECMA2016 und Java 8 ähneln sich die beiden Programmiersprachen immer mehr. Eine standardisierte Übersetzung von Java direkt nach JavaScript rückt damit in greifbare Nähe.

 

Bildnachweis: pixabay.com, CCO License @unsplash

Weiterlesen