Quarkus + Fabric8 Maven Plugin + GraalVM
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:
1mvn io.quarkus:quarkus-maven-plugin:0.26.1:create \
2 -DprojectGroupId=com.marcnuri.demo \
3 -DprojectArtifactId=fmp-quarkus \
4 -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”.
1public class Quote implements Serializable {
2 /** ... **/
3 private String content;
4 private String author;
5 /** ... **/
6}
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.
1@Singleton
2public class QuoteService {
3
4 private static final Logger log = LoggerFactory.getLogger(QuoteService.class);
5
6 private static final String QUOTES_RESOURCE= "/quotes/quotes.json";
7
8 private final List<Quote> quotes;
9
10 public QuoteService() {
11 quotes = new ArrayList<>();
12 }
13
14 @PostConstruct
15 protected final void initialize() {
16 final ObjectMapper objectMapper = new ObjectMapper();
17 try (final InputStream quotesStream = QuoteService.class.getResourceAsStream(QUOTES_RESOURCE)) {
18 quotes.addAll(objectMapper.readValue(quotesStream,
19 objectMapper.getTypeFactory().constructCollectionType(List.class, Quote.class)));
20 } catch (IOException e) {
21 log.error("Error loading quotes", e);
22 }
23 }
24
25
26 Quote getRandomQuote() {
27 return quotes.get(ThreadLocalRandom.current().nextInt(quotes.size()));
28 }
29
30}
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.
1@Path("/quotes")
2public class QuoteResource {
3
4 private static final String HEADER_QUOTE_AUTHOR = "Quote-Author";
5
6 private QuoteService quoteService;
7
8 @GET
9 @Path("/random")
10 public Response getRandomQuote() {
11 final Quote randomQuote = quoteService.getRandomQuote();
12 return Response
13 .ok(randomQuote.getContent(), MediaType.TEXT_PLAIN_TYPE)
14 .header(HEADER_QUOTE_AUTHOR, randomQuote.getAuthor())
15 .build();
16 }
17
18 @Inject
19 public void setQuoteService(QuoteService quoteService) {
20 this.quoteService = quoteService;
21 }
22}
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:
1./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:
1<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:
1./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:
1./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:
1<build>
2 <plugins>
3 <!-- ... -->
4 <plugin>
5 <groupId>io.fabric8</groupId>
6 <artifactId>fabric8-maven-plugin</artifactId>
7 <version>${fmp.version}</version>
8 <configuration>
9 <images>
10 <image>
11 <name>marcnuri/fmp-quarkus:jvm</name>
12 <build>
13 <contextDir>${project.basedir}</contextDir>
14 <dockerFile>src/main/docker/Dockerfile.jvm</dockerFile>
15 </build>
16 </image>
17 </images>
18 <authConfig>
19 <username>${env.DOCKER_HUB_USER}</username>
20 <password>${env.DOCKER_HUB_PASSWORD}</password>
21 </authConfig>
22 </configuration>
23 </plugin>
24 </plugins>
25</build>
La primera parte (groupId
, artifactId
& version
) indica que queremos emplear Fabric8 Maven Plugin. En michos 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:
1./mvnw clean package fabric8:build
Imagen Docker ejecutando la aplicación ne 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:
1<profiles>
2 <profile>
3 <id>native</id>
4 <!-- ... -->
5 <build>
6 <plugins>
7 <!-- ... -->
8 <plugin>
9 <groupId>io.fabric8</groupId>
10 <artifactId>fabric8-maven-plugin</artifactId>
11 <version>${fmp.version}</version>
12 <configuration>
13 <images>
14 <image>
15 <name>marcnuri/fmp-quarkus:native</name>
16 <build>
17 <contextDir>${project.basedir}</contextDir>
18 <dockerFile>src/main/docker/Dockerfile.native</dockerFile>
19 <tags>
20 <tag>latest</tag>
21 </tags>
22 </build>
23 </image>
24 </images>
25 <authConfig>
26 <username>${env.DOCKER_HUB_USER}</username>
27 <password>${env.DOCKER_HUB_PASSWORD}</password>
28 </authConfig>
29 </configuration>
30 </plugin>
31 </plugins>
32 </build>
33 </profile>
34</profiles>
En este caso, vamos a modificar la configuración de la imagen. En primer lugar 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:
1./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:
1./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.