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