Quarkus + Fabric8 Maven Plugin + GraalVM
Nota
El plugin Fabric8 Maven ha sido deprecado y migrado a Eclipse JKube.
Puedes encontrar una versiĆ³n actualizada de este artĆculo utilizando Eclipse JKube aquĆ.
IntroducciĆ³n
En este tutorial veremos como desarrollar una aplicaciĆ³n muy sencilla con Quarkus e integrarla con Fabric8 Maven Plugin para publicar una imagen nativa con GraalVM en Docker Hub.
La primera parte de la publicaciĆ³n describe como desarrollar una simple aplicaciĆ³n con Quarkus. La siguiente parte describe como construir un ejecutable nativo con GraalVM. La Ćŗltima secciĆ³n muestra como integrar el proyecto con Fabric8 Maven Plugin y cĆ³mo desplegar las diferentes imĆ”genes de la aplicaciĆ³n en Docker Hub.
Quarkus, aplicaciĆ³n de muestra
En esta secciĆ³n se muestra cĆ³mo desarrollar una aplicaciĆ³n muy sencilla que devolverĆ” una cita al azar cada vez que se haga una peticiĆ³n al endpoint /quotes/random
.
Proyecto inicial
Si Maven estƔ instalado, la forma mƔs sencilla de iniciar el proyecto es lanzando el siguiente comando:
mvn io.quarkus:quarkus-maven-plugin:0.26.1:create \\
-DprojectGroupId=com.marcnuri.demo \\
-DprojectArtifactId=fmp-quarkus \\
-DclassName="com.marcnuri.demo"
Esto crearĆ” un nuevo directorio fmp-quarkus
con un proyecto inicial de Maven con soporte para Maven Wrapper.
Si Maven no estĆ” disponible, o simplemente porque prefieres una interfaz grĆ”fica, puedes navegar a code.quarkus.io y descargar un proyecto inicial con tus requisitos especĆficos.

Recursos del proyecto
Tal como se ha explicado, el sencillo propĆ³sito de la aplicaciĆ³n es devolver una cita al azar cada vez que un usuario haga una peticiĆ³n a un endpoint.
Las citas se cargarƔn desde un fichero JSON situado en la carpeta de recursos del proyecto.
Para ello, aƱadiremos el fichero quotes.json al directorio src/main/resources/quotes/
.
Endpoint para obtener una cita al azar
El primer paso es crear un Pojo (Quote) que se emplearĆ” para āmapearā las citas del fichero JSON, aƱadido en el paso anterior, cuando Ć©ste se ādeserialiceā.
public class Quote implements Serializable {
/* ... */
private String content;
private String author;
/* ... */
}
Para poder leer las citas desde el directorio de recursos y exponer una de ellas al azar, crearemos una clase QuoteService que se encargue de estas tareas.
@Singleton
public class QuoteService {
private static final Logger log = LoggerFactory.getLogger(QuoteService.class);
private static final String QUOTES_RESOURCE= "/quotes/quotes.json";
private final List<Quote> quotes;
public QuoteService() {
quotes = new ArrayList<>();
}
@PostConstruct
protected final void initialize() {
final ObjectMapper objectMapper = new ObjectMapper();
try (final InputStream quotesStream = QuoteService.class.getResourceAsStream(QUOTES_RESOURCE)) {
quotes.addAll(objectMapper.readValue(quotesStream,
objectMapper.getTypeFactory().constructCollectionType(List.class, Quote.class)));
} catch (IOException e) {
log.error("Error loading quotes", e);
}
}
Quote getRandomQuote() {
return quotes.get(ThreadLocalRandom.current().nextInt(quotes.size()));
}
}
El mƩtodo initialize
utilizarĆ” Jackson para leer y deserializar el fichero quotes.json en un ArrayList
que se emplearĆ” posteriormente para obtener una cita aleatoria.
El mƩtodo getRandomQuote
serĆ” el encargado de devolver una entrada Quote
al azar desde el ArrayList
inicializado en el mƩtodo descrito en el pƔrrafo anterior.
El Ćŗltimo paso es exponer las citas a travĆ©s de un endpoint REST. Para ello crearemos la clase QuoteResource.
@Path("/quotes")
public class QuoteResource {
private static final String HEADER_QUOTE_AUTHOR = "Quote-Author";
private QuoteService quoteService;
@GET
@Path("/random")
public Response getRandomQuote() {
final Quote randomQuote = quoteService.getRandomQuote();
return Response
.ok(randomQuote.getContent(), MediaType.TEXT_PLAIN_TYPE)
.header(HEADER_QUOTE_AUTHOR, randomQuote.getAuthor())
.build();
}
@Inject
public void setQuoteService(QuoteService quoteService) {
this.quoteService = quoteService;
}
}
La clase tiene un mƩtodo getRandomQuote
que emplearĆ” una instancia de QuoteService
para obtener una cita al azar y devolver su contenido en el cuerpo de la respuesta HTTP.
El autor de la cita tambiƩn estarƔ disponible en una de las cabeceras de la respuesta.
Una vez completados todos estos pasos, podemos iniciar la aplicaciĆ³n en modo desarrollo con el siguiente comando:
./mvnw clean quarkus:dev

El endpoint estarĆ” ahora accesible en http://localhost:8080/quotes/random.

Construyendo un ejecutable nativo con GraalVM
El siguiente paso es construir la aplicaciĆ³n empleando GraalVM para crear una imagen nativa.
Lo primero serĆ” adaptar la aplicaciĆ³n para que sea compatible con GraalVM.
Incluir recursos
Por defecto, GraalVM no incluirĆ” ninguno de los recursos disponibles en el classpath durante la creaciĆ³n de la imagen nativa. Los recursos que deban de estar disponibles en tiempo de ejecuciĆ³n deben de incluirse especĆficamente cuando se cree la imagen.
Con este propĆ³sito, modificaremos el fichero pom.xml de nuestra aplicaciĆ³n y aƱadiremos la siguiente lĆnea:
<additionalBuildArgs> -H:IncludeResources=.*\\.json$</additionalBuildArgs>
Esto harĆ” que Quarkus incluya la opciĆ³n -H:IncludeResources
en la lĆnea de comandos cuando ejecute el comando native-image
. En este caso le estamos diciendo que se incluyan todos los ficheros terminados en .json
.
Java Reflection en imƔgenes nativas
La librerĆa de Jackson encargada de la deserializaciĆ³n de JSONs emplea Java Reflection para crear instancias de las clases que se estĆ”n deserializando durante la lectura del JSON. Para la construcciĆ³n de imĆ”genes nativas, Graal requiere saber antes de tiempo a quĆ© tipo de elementos se accede mediante Java Reflection durante la ejecuciĆ³n de la aplicaciĆ³n.
Quarkus facilita esta tarea al proveernos de la anotaciĆ³n @RegisterForReflection
que automatiza esta tarea. Para nuestra aplicaciĆ³n de ejemplo, anotaremos la clase Quote con ella.
Construyendo la aplicaciĆ³n
Una vez completadas todas las modificaciones, podemos proceder a la construcciĆ³n de la aplicaciĆ³n en modo nativo. Si GraalVM con soporte para native-image estĆ” disponible en nuestro sistema, podemos ejecutar el siguiente comando:
./mvnw clean package -Pnative

Esto crearĆ” el ejecutable nativo en el directorio target.
Ahora podemos ejecutar nuestra aplicaciĆ³n de forma nativa:

El endpoint volverĆ” a estar accesible nuevamente en http://localhost:8080/quotes/random.
Si GraalVM no estĆ” disponible en nuestro sistema, pero sĆ Docker, el mismo comando puede ejecutarse por medio de un contenedor de Docker:
./mvnw clean package -Pnative -Dnative-image.docker-build=true
Fabric8 Maven Plugin
El Ćŗltimo paso del tutorial es integrar el proceso de construcciĆ³n con Fabric8 Maven Plugin.
Vamos a construir (fabric8:build
) 2 imƔgenes diferentes dependiendo del perfil de Maven que seleccionemos.
Para ello, vamos a reutilizar los ficheros Docker que fueron generados junto al proyecto inicial.
El proyecto inicial contiene dos ficheros Dockerfiles distintos en el directorio src/main/docker
,
uno que crea una imagen que ejecuta la aplicaciĆ³n Quarkus en modo JVM y otro que ejecuta la aplicaciĆ³n en modo nativo.
Imagen Docker ejecutando la aplicaciĆ³n mediante JVM
Vamos a modificar el build por defecto de nuestro pom.xml para construir una imagen Docker que ejecute nuestra aplicaciĆ³n igual que cualquier otra aplicaciĆ³n Java empleando una mĆ”quina virtual de Java. En este caso, vamos a utilizar el archivo Dockerfile.jvm.
En la secciĆ³n de plugins del build por defecto insertaremos las siguientes lĆneas:
<build>
<plugins>
<!-- ... -->
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>fabric8-maven-plugin</artifactId>
<version>\${fmp.version}</version>
<configuration>
<images>
<image>
<name>marcnuri/fmp-quarkus:jvm</name>
<build>
<contextDir>\${project.basedir}</contextDir>
<dockerFile>src/main/docker/Dockerfile.jvm</dockerFile>
</build>
</image>
</images>
<authConfig>
<username>\${env.DOCKER_HUB_USER}</username>
<password>\${env.DOCKER_HUB_PASSWORD}</password>
</authConfig>
</configuration>
</plugin>
</plugins>
</build>
La primera parte (groupId
, artifactId
y version
) indica que queremos emplear Fabric8 Maven Plugin.
En muchos casos esto podrĆa ser suficiente ya que el plugin incluye un modo Zero-Config que funciona con valores por defecto.
A continuaciĆ³n aƱadimos una configuraciĆ³n especĆfica para nuestra imagen.
Primero definimos el nombre de la imagen marcnuri/fmp-quarkus:jvm
. Esto crearĆ” una imagen para el repositorio marcnuri con el nombre fmp-quarkus y el tag jvm
.
Es muy importante aƱadir el tag de esta forma, si no la imagen tambiƩn recibirƔ el tag latest
(ver configuraciĆ³n para imĆ”genes nativas).
TambiƩn estamos especificando el dockerFile
que queremos emplear en combinaciĆ³n con contextDir
(directorio raĆz del proyecto).
Por Ćŗltimo aƱadimos las credenciales de Docker Hub en la secciĆ³n authConfig
.
El goal de Maven para publicar las imĆ”genes en Docker Hub sĆ³lo se emplearĆ” a travĆ©s de GitHub Actions CI,
las credenciales estarƔn disponibles en las variables de entorno especificadas.
Podemos lanzar el siguiente comando para iniciar la construcciĆ³n de la imagen Docker:
./mvnw clean package fabric8:build


Imagen Docker ejecutando la aplicaciĆ³n en modo nativo
Del mismo modo que hemos hecho para construir la imagen en modo JVM, vamos a aƱadir el plugin al perfil native con algunos ajustes especĆficos:
<profiles>
<profile>
<id>native</id>
<!-- ... -->
<build>
<plugins>
<!-- ... -->
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>fabric8-maven-plugin</artifactId>
<version>\${fmp.version}</version>
<configuration>
<images>
<image>
<name>marcnuri/fmp-quarkus:native</name>
<build>
<contextDir>\${project.basedir}</contextDir>
<dockerFile>src/main/docker/Dockerfile.native</dockerFile>
<tags>
<tag>latest</tag>
</tags>
</build>
</image>
</images>
<authConfig>
<username>\${env.DOCKER_HUB_USER}</username>
<password>\${env.DOCKER_HUB_PASSWORD}</password>
</authConfig>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
En este caso, vamos a cambiar el nombre de la imagen por marcnuri/fmp-quarkus:native
para que reciba el tag native
.
TambiƩn vamos a utilizar Dockerfile.native como nuestro dockerFile
.
TambiƩn queremos que la imagen reciba el tag latest
(serĆ” la imagen que se descargarĆ” por defecto si no se especifica ningĆŗn tag), para ello aƱadiremos este tag en la secciĆ³n tags
.
El mismo resultado se podrĆa haber obtenido si hubiĆ©semos especificado el nombre de la imagen si ningĆŗn tag (i.e. marcnuri/fmp-quarkus
) y aƱadido el tag native
a la secciĆ³n tags
.
Podemos ejecutar el siguiente comando para construir la imagen Docker nativa:
./mvnw clean package fabric8:build -Pnative


fabric8:push Publicando la imagen en Docker Hub
Si tenemos las variables de entorno adecuadas (authentication), ahora podemos ejecutar el siguiente comando para publicar la imagen en Docker Hub repository:
./mvnw fabric8:push
En nuestro caso, vamos a ejecutar este paso desde un workflow de GitHub Actions:

ConclusiĆ³n
Esta publicaciĆ³n muestra como desarrollar una sencilla aplicaciĆ³n empleando Quarkus y cĆ³mo publicar en Docker Hub una imagen Docker que ejecuta un binario nativo creado con GraalVM mediante Fabric8 Maven Plugin. En la primera secciĆ³n hemos aprendido a construir un endpoint REST que devolverĆ” una cita aleatoria para cada una de las peticiones. El segundo paso muestra como adaptar la aplicaciĆ³n para que pueda ser convertida en un ejecutable nativo con GraalVM. Por Ćŗltimo, hemos integrado Fabric8 Maven Plugin en nuestro fichero pom.xml para poder construir y publicar dos imĆ”genes Docker distintas.
El cĆ³digo fuente completo de este artĆculo puede encontrarse en Github.
