Hilla-Anwendungen als Native Image deployen

René Wilby | 17.06.2024 Min. Lesezeit

Hilla-Anwendung als Native Image für Deployment bauen

Eine Hilla-Anwendung, die über die Hilla-CLI erstellt wurde, kann via Maven (oder Gradle) für das Deployment als Native Image gebaut werden. Dazu ist ein JDK mit GraalVM-Unterstützung erforderlich. Eine Hilla-Anwendung, die als Native Image gebaut wurde, ist eine eigenständige ausführbare Datei, die ohne eine Java-Laufzeitumgebung ausgeführt werden kann. Im Vergleich zu einer Hilla-Anwendung, die als JAR-Datei gebaut wurde und ausgeführt wird, zeichnet sich eine Hilla-Anwendung, die als Native Image gebaut wurde und ausgeführt wird, i.d.R. durch eine geringere Startzeit und eine geringere Speichernutzung aus. Eine Hilla-Anwendung kann mit Unterstützung von GraalVM wie folgt als Native Image gebaut werden:

./mvnw clean package -Pproduction -Pnative native:compile

Das erstellte Native Image enthält alle erforderlichen Klassen, Abhängigkeiten und das Frontend-Bundle. Da eine Hilla-Anwendung ein klassisches Spring Boot-Projekt ist, enthält das erstellte Native Image auch einen integrierten Server, der die Hilla-Anwendung bereitstellt, sobald das Native Image ausgeführt wird.

Weitere Informationen zur Unterstützung von Native Images durch Spring Boot sind hier zu finden.

Hilla-Anwendung als Native Image ausführen

Die gebaute Hilla-Anwendung my-app kann als Native Image folgendermaßen ausgeführt werden:

./target/my-app

Spring Boot-spezifische Konfigurationen können hierbei bei Bedarf als Umgebungsvariablen gesetzt bzw. übersteuert werden. So kann bspw. das Log-Level beim Start der Anwendung folgendermaßen angepasst werden:

export LOGGING_LEVEL_COM_VAADIN=DEBUG && ./target/my-app

Docker-Image für Hilla-Anwendung erstellen

Das Deployment von Anwendungen findet häufig auf Basis von Containern statt. Daher bietet es sich an für die Hilla-Anwendung ein Dockerfile zu erstellen. Das Dockerfile sollte im Basis-Verzeichnis der Hilla-Anwendung liegen und folgenden Inhalt haben:

FROM debian:bookworm-slim

WORKDIR /app

COPY target/*.so /app/
COPY target/my-app /app/my-app

EXPOSE 8080

CMD ["/app/my-app"]

Das Dockerfile basiert auf einem abgespeckten Basis-Image, hier debian:bookworm-slim. Das Native Image der Hilla-Anwendung wird zusammen mit den beim Bau erstellten Bibliotheken (.so Dateien) in das Docker-Image kopiert. Die Bibliotheken stellen Abhängigkeiten bereit, die nicht Bestandteil des Native Image sind, aber von der Anwendung während der Ausführung benötigt werden. Des weiteren enthält das Dockerfile die Konfiguration für den Port, über den die Anwendung Anfragen erhalten kann. Über CMD wird festgelegt, dass das Native Image im Docker-Image beim Start des Containers ausgeführt werden soll.

Docker-Image lokal erstellen

Das Docker-Image kann lokal mit Docker oder Podman erstellt werden:

docker|podman build --tag my-app .

Der Befehl wird im Basis-Verzeichnis der Hilla-Anwendung ausgeführt. Der . zeigt dabei auf das selbige Verzeichnis und das darin befindliche Dockerfile.

Das Docker-Image ist anschließend lokal verfügbar und verwendbar.

Docker-Container lokal starten

Auf Basis des erstellten Docker-Images kann ein Docker-Container mit der Hilla-Anwendung gestartet werden:

docker|podman run -it --rm --publish 8080:8080 my-app

Dockerfile um Build-Stage erweitern

Das Bauen der Hilla-Anwendung als Native Image kann auch Bestandteil des Dockerfile sein. Dafür wird das Dockerfile in zwei Stages aufgeteilt:

# First stage: JDK with GraalVM and native image support
FROM ghcr.io/graalvm/native-image-community:21.0.2 AS build

WORKDIR /usr/src/app

# Copy project files 
COPY . .

# Create native image
RUN ./mvnw clean package -Pproduction -Pnative native:compile

# Second stage: Lightweight debian-slim image
FROM debian:bookworm-slim

WORKDIR /app

# Copy the produced library files from the build stage
COPY --from=build /usr/src/app/target/*.so /app/
# Copy the native image from the build stage
COPY --from=build /usr/src/app/target/my-app /app/my-app

EXPOSE 8080

# Run the application
CMD ["/app/my-app"]

In der ersten Stage des Dockerfile wird das Native Image erzeugt. Dazu werden die erforderlichen Dateien und Abhängigkeiten in das Docker-Image kopiert und anschließend die Hilla-Anwendung als Native Image gebaut. In der zweiten Stage wird das Docker-Image mit dem gebauten Native Image erstellt.

Hilla-Anwendung über fly.io deployen

Mit Hilfe der erstellen Dockerfile kann eine Hilla-Anwendung sehr einfach über einen Dienst wie fly.io deployed werden. Die Registrierung bei fly.io ist kostenlos, erfordert jedoch die Angabe von Kreditkarten-Daten. Fly.io bietet einen kostenlosen Hobby-Plan, über den man kostenlos Anwendungen mit eingeschränkten Ressourcen deployen kann. Die Anwendung ist anschließend über https://<unique-app-identifier>.fly.dev erreichbar. Die erforderliche Konfiguration von IP-Adressen, DNS, SSL-Zertifikat usw. übernimmt dabei fly.io.

Der Weg zum Deployment ist in https://fly.io/docs/hands-on/ ausführlich beschrieben.

Zunächst wird die flyctl installiert. Unter macOS kann dies bspw. mit brew erfolgen:

brew install flyctl

Anschließend wird ein neues Konto bei fly.io erstellt oder ein bestehendes Konto verknüpft:

fly auth signup

Vor dem ersten Deployment wird eine Deployment-Konfiguration für fly.io erstellt. Dies erfolgt über den Befehl:

fly launch --dockerfile Dockerfile

Die vorgeschlagenen Einstellungen können übernommen werden. Eine .dockerignore sollte zunächst nicht erstellt werden, da dies ansonsten das Kopieren des Native Image und der Bibliotheken vorerst verhindern würde.

Die Deployment-Konfiguration legt die flyctl in der Datei fly.toml ab. Diese Datei kann bei Bedarf editiert werden.

app = '<unique-app-identifier>'
primary_region = 'ams'

[build]
  dockerfile = 'Dockerfile'

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 0
  processes = ['app']

[[vm]]
  size = 'shared-cpu-1x'
  memory = '256mb'

Das eigentliche Deployment der Hilla-Anwendung erfolgt dann mittels:

fly deploy

Das Dockerfile, das Native Image und die Bibliotheken (.so Dateien) werden auf einen Build-Server von fly.io hochgeladen. Das Erstellen des Docker-Images passiert bei fly.io. Fly.io legt das erstellte Docker-Image in einer eigenen Registry ab und startet anschließend die Hilla-Anwendung als Docker-Container. Nach erfolgreichem Start kann die Anwendung im Browser über https://<unique-app-identifier>.fly.dev aufgerufen werden.

Wenn man das oben gezeigte Dockerfile mit den zwei Stages verwenden möchte, muss man beachten, dass dafür die verfügbaren Ressourcen des Hobby-Plans voraussichtlich nicht ausreichend sind und der Bau-Vorgang des Docker-Images bei fly.io deshalb fehlschlagen kann.

Hilla-Anwendung aktualisieren

Ergeben sich Änderungen am Code der Hilla-Anwendung muss diese erneut gebaut und anschließend erneut deployed werden:

./mvnw clean package -Pproduction -Pnative native:compile
fly deploy

Erfolgt das Bauen des Native Image über eine eigene Stage im Dockerfile ist nur die Ausführung von fly deploy erforderlich.

Fazit

Eine Hilla-Anwendung kann mit Unterstützung von GraalVM sehr einfach für das Deployment als Native Image gebaut werden. Mit Hilfe eines passenden Dockerfile steht dem Deployment über einen Dienst wie fly.io nichts im Weg. Die offizielle Dokumentation von Hilla bietet weitere Anleitungen für das Deployment von Hilla-Anwendungen, z.B. zu AWS, GCP, Azure oder Heroku.

Im Hilla-Blog findet sich darüber hinaus auch ein Blog-Post von Marcus Hellberg zu diesem Thema: Deploying a Spring Boot app as a native GraalVM image with Docker.

Eine Beispiel-Hilla-Anwendung, die als Native Image gebaut und über fly.io deployed wurde, kann über https://vaadin-create-countdown.fly.dev/ aufgerufen werden. Den zugehörigen Quellcode findet man bei GitHub.