Neuigkeiten von trion.
Immer gut informiert.

Docker für lokale Tests mit Angular und Spring Boot

Dieser Artikel zeigt, wie sich mit wenigen Schritten eine komfortable und leicht zu wartende Umgebung für Entwicklung und lokale Tests von Java Spring-Boot und TypeScript Angular Anwendungen erstellen lässt.

Vorweg stellt sich die Frage: Warum sind neue Ansätze sinnvoll und was steckt hinter den eingesetzten Technologien?

Durch die strikte Trennung von Frontend und Backend-Anwendungen (API) erhöht sich die Anzahl der für Entwicklertests gemeinsam zu startenden Komponenten. Hatte man bisher eine einzelne Anwendung, die möglicherweise mit einer embedded-Datenbank und einem Applicationserver im Java EE Umfeld oder sogar standalone bei einem Spring Projekt betrieben werden konnte, so findet man aufgrund von Microservicearchitekturen meist sogar mehrere separate API Systeme. Dank Docker als Containersystem lassen sich nun jedoch sehr schnell und leichtgewichtig gesamte Umgebungen zusammenstellen. Dadurch wird sogar der Einsatz von eingebetteten Datenbanken - und den damit einhergehenden Kompatibilitätsproblemen - oft verzichtbar.

Motivation

Ein bewährter Technologie-Stack im Java Umfeld besteht aus Spring-Boot für die Backend Services und einem JavaScript Framework für das Frontend. Für das Frontend ist Angular (Angular 2 / "Just Angular") als Fullstack-Framework eine häufige Wahl und wird daher auch für das Beispiel verwendet.

Geht es nun um den Test bei einer solchen Anwendung, so werden folgende Bestandteile benötigt:

  • Infrastruktur-Komponenten wie z.B. Datenbank, Mailserver und Message-Queue

  • Backend Anwendung(en)

  • Frontend (Client)

Während man eine Spring Boot Anwendung sehr einfach lokal starten kann, beispielsweise aus der IDE heraus oder mittels mvn spring-boot:run, ist das mit dem Frontend nicht ganz so einfach, auch wenn es sich dabei im Prinzip nur um statische HTML und JavaScript-Dateien handelt.

Sicherheitsfeatures im Browser verhindern, dass lokal aus dem Dateisystem geladene Dateien alle erforderlichen Operationen ausführen können. Zudem ist es in der Regel so, dass moderne Webanwendungen nicht mehr direkt als (ausführbares) JavaScript erstellt werden. Stattdessen wird z.B. TypeScript verwendet, wodurch ein zusaetzlicher Build erforderlich wird, der aus den Quelltexten eine Anwendung mit der von allen Browsern unterstützen JavaScript Version generiert.

Auch wenn ein Start aus der IDE heraus durch einen Mini-Webserver in allen modernen IDEs unterstützt wird, so ist nicht jeder Tester oder Backend-Entwickler daran interessiert, die Angular Anwendung durch eine IDE zu starten. Auch umgekehrt soll es Entwickler geben, die das Java-Tooling nicht installieren und warten möchten, wenn ihr Fokus die Entwicklung moderner JavaScript Frontends ist.

Man könnte nun das Frontend als Teil des sowieso erforderlichen Builds der Backend Anwendung erstellen. Damit gäbe es nur einen Build und man hätte alle Bestandteile vereinigt, so dass die Backend-Anwendung das Frontend ausliefern könnte. Dagegen sprechen drei Dinge:

  • Der Build von modernen JavaScript Anwendungen wie beispielsweise Angular nutzt deutlich andere Werkzeuge als der Java Build. Hier kommen Werkzeuge zum Einsatz, die selbst oft in JavaScript erstellt werden. Wuerde man beides mit einem Build adressieren, so benötigt man zwingend ein Buildsystem, welches die Vereinigungsmenge der Java und JavaScript/Node Werkzeuge installiert hat.

  • Zudem gibt es oft die Situation, dass die Entwicklungsgeschwindigkeit von Backend und Frontend unterschiedlich ist. Dem wird man am besten durch getrennte Build- und Releasezyklen und entsprechend getrennte Source-Repositories gerecht.

  • Ein ebenfalls relevanter Punkt kann sein, wenn es mehr als ein Backend gibt: In Microservicearchitekturen kann man das Frontendprojekt nicht zwangsläufig genau einem Backendprojekt zuordnen.

Spring Boot in Docker

Die Spring Boot Anwendung benötigt als Umgebung lediglich eine Java Runtime, z.B. OpenJDK. Von OpenJDK gibt es ein offizielles Dockerimage, welches hier als Basisimage verwendet wird. Für das Beispiel wird eine sehr simple Spring Boot Anwendung verwendet, die lediglich einen String per HTTP Schnittstelle liefert.

Um die Anwendung zu erzeugen, verwendet man am besten die Projektvorlagen von Spring: Auf https://start.spring.io/ werden als Komponenten actuator, web, und devtools ausgewählt und das vorbereitete Projekt in der zu verwendenden IDE importiert.

Der Controller zur Ausgabe des String sieht dann wie folgt aus:

@RestController
public class GreetingController
{
  @Value("${sample.greeting:'hello there'}")  //(1)
  private String greeting;

  @GetMapping("/greeting")
  public String greeting()
  {
     return greeting;
  }
}

Um die Möglichkeiten zur Konfiguration der Anwendung über Umgebungsvariablen aufzuzeigen, wird der zu liefernde Wert von Spring ermittelt und per Dependency-Injection bereitgestellt. Spring wertet die spezielle Umgebungsvariable SPRING_APPLICATION_JSON aus, um daraus Werte für Properties im Spring Kontext abzuleiten. Das Dockerfile für eine Springanwendung kann darüber Standardwerte bereitstellen, wie im folgenden Beispiel gezeigt.

Nachdem das Projekt erfolgreich mit Maven oder Gradle baut, muss das erzeugte Artefakt nun in ein Docker Image verpackt werden.

Beispiel für ein Dockerfile für eine Spring-Boot Anwendung:

FROM openjdk:8                  #(1)
VOLUME /tmp                     #(2)

ENV SPRING_APPLICATION_JSON=\
'{"sample": {"greeting": "Hello from docker!" }}'  #(3)

EXPOSE 8080                     #(4)

COPY target/spring-app*.jar /app/app.jar    #(5)

#(6)
CMD ["java","-XX:+UnlockExperimentalVMOptions",\
"-XX:+UseCGroupMemoryLimitForHeap","-XX:MaxRAMFraction=1",\
"-Djava.security.egd=file:/dev/./urandom","-jar","/app/app.jar"]
  1. Verwendung von OpenJDK als Basisimage

  2. In /tmp wird durch Spring geschrieben

  3. Spring Default-Environment

  4. Deklaration des bereitgestellten Ports

  5. Gebautes Anwendungsartefakt dem Docker Image hinzufügen

  6. Startet die Spring Anwendung, wenn der aus dem Image erzeugte Container gestartet wird

Das Docker CMD hat einige Parameter gesetzt, die die JVM für den Einsatz in einem Container optimieren. Über das CMD lassen sich auch weitere Einstellungen vornehmen, um zum Beispiel einen (remote-)Debugger aus der IDE zuzulassen:

CMD ["java",
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=9009",\
"-jar","-Djava.security.egd=file:/dev/./urandom",\
"/app/app.jar"]

Das muss dann nicht im Dockerimage selbst geschehen, sondern CMD lässt sich auch zur Startzeit des Containers überschreiben.

Die Java Anwendung wird für das Beispiel mittels mvn clean package gebaut - das Buildergebnis wird für das Docker Image benötigt.

Um das Image zu bauen und unter dem Namen trion/spring-app verfügbar zu machen, wird aus dem Verzeichnis der Spring Anwendung bzw. des Dockerfiles folgendes Kommando ausgeführt:

docker build -t trion/spring-app .

Angular in Docker

Angular Anwendungen werden durch einen Webbrowser ausgeführt und durch einen Webserver ausgeliefert. Da die Angular Anwendung lediglich aus statischen HTML, CSS und JavaScript Dateien besteht, bietet sich ein schlanker Webserver, wie beispielsweise nginx an. Lediglich für das Angular Routing müssen ein paar Rewrite-Regeln erstellt werden, damit auch der Einsprung in eine Route bzw. Neuladen korrekt funktioniert.

Die zugehörige nginx Konfiguration sieht dafür so aus:

server {
  listen 8080;

  location / {
    root /usr/share/nginx/html;
    index index.html index.htm;
    try_files $uri$args $uri$args/ $uri $uri/ /index.html =404;
  }
}

Fehlt nur noch ein Dockerfile, um für die Angular Anwendung ein Image mit nginx zu definieren. Für das Beispiel wird angenommen, dass der Angular Frontendbuild mit Angular-CLI umgesetzt ist, und die Buildergebnisse im dist Ordner landen. Damit sieht das Dockerfile wie folgt aus:

FROM nginx:alpine

EXPOSE 8080
COPY dist /usr/share/nginx/html/
COPY nginx/default.conf /etc/nginx/conf.d/default.conf.template

RUN chown -R nginx /etc/nginx

CMD ["/bin/sh","-c",\
"cp /etc/nginx/conf.d/default.conf.template\
 /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"]

Die nginx Konfiguration kann dann z.B. in einem nginx Unterordner des Angularprojekts abgelegt werden.

Nachdem die Angular Anwendung mittels ng build gebaut wurde, kann das Resultat zu einem Image festgeschrieben werden.

Um das Image zu bauen und unter dem Namen trion/angular-app verfügbar zu machen, wird aus dem Verzeichnis mit der Angular Anwendung folgendes Kommando verwendet:

docker build -t trion/angular-app .

Automatisierung des Builds

Der Build der Komponenten besteht aus relativ vielen Schritten. Damit bietet sich eine Automatisierung der Abläufe an. Für lokale Tests kann das ein Shellscript oder ein Makefile sein, für Umgebungen mit mehreren Entwicklern wählt man hier sinnvollerweise einen Buildserver, wie beispielsweise Jenkins. Bei Änderungen an den Quellen werden dann entsprechend die Docker Images erstellt und einer Docker Registry zur Verfügung gestellt.

Spring Boot und Angular Komposition: docker-compose

Die einzelnen Container händisch zu verwalten ist umständlich und fehleranfällig. Als Alternative zu individuellem Scripting bietet docker-compose eine Lösung zur Verwaltung von Container-Zusammenstellungen. Mit docker-compose lässt sich genau angeben, welche Container benötigt werden und welche Ressourcen, wie Netzwerkports, konfiguriert werden sollen.

Bei Verwendung einer zentralen Registry ist die docker-compose Konfiguration relativ einfach, es werden einfach die zu verwendenden Images angegeben:

  version: "2.1"
  services:
    backend:
      image: trion/spring-app
      ports:
        - "8081:8080"
        - "9009:9009"
    frontend:
      image: trion/angular-app
      ports:
        - "8080:8080"
      links:
        - backend

Wenn die Images nicht durch eine zentrale Registry bereitgestellt werden, so kann docker-compose auch zum Build der Docker Images genutzt werden. Das setzt allerdings voraus, dass die für die Images benötigten Artefakt-Builds bereits stattgefunden haben, also das Spring Boot JAR und der Angular "dist" Ordner erstellt sind. Bei lokaler Entwicklung bietet sich hier ein entsprechendes Scripting an, dass auch den docker-compose Aufruf beinhaltet.

version: "2.1"
services:
  backend:
    build:
      context: ./spring-app
    ports:
      - "8081:8080"
      - "9009:9009"
  frontend:
    build:
      context: ./angular-app
    ports:
      - "8080:8080"
    links:
      - backend

Build container

Docker lässt sich auch hervorragend als Container für den Build der Artefakte selbst einsetzen. Damit werden keine Abhängigkeiten auf den Entwicklerrechnern benötigt und zusätzlich wird der Build deutlich besser reproduzierbar.

Für das Springprojekt könnte das mit dem offiziellen Docker-Maven Image umgesetzt werden:

docker pull maven:alpine
docker run -it --rm -u $(id -u) -v "$PWD":/usr/src/mymaven -w /usr/src/mymaven maven:alpine mvn clean install

docker build -t trion/spring-app .

Für die Angular Anwendung kann das Angular CLI Image trion/ng-cli-karma genutzt werden:

docker pull trion/ng-cli-karma
docker run -u $(id -u) --rm -v "$PWD":/app trion/ng-cli npm install && ng build --prod --aot --sourcemaps

docker build -t trion/angular-app .

Ein vollständiges Beispielprojekt findet sich hier: https://github.com/everflux/angular-boot-docker.git




Zu den Themen Angular, Docker und Spring Boot bieten wir sowohl Unterstützung als auch passende Schulungen an:

Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Zur Desktop Version des Artikels