Quarkus 2 + Kubernetes Maven Plugin + GraalVM integration
Introducción
En este tutorial os enseñaré como desarrollar e integrar una aplicación Quarkus 2 muy sencilla con Kubernetes Maven Plugin (Eclipse JKube) para publicar una imagen nativa GraalVM en Docker Hub y cómo desplegarla en Kubernetes.
Esta es una actualización de mi artículo Quarkus + Fabric8 Maven Plugin + GraalVM, y que Fabric8 Maven Plugin está obsoleto y debe de reemplazarse por JKube.
En la primera parte describo como desarrollar una aplicación muy sencilla utilizando Quarkus. A continuación, muestro como compilar un ejecutable nativo empleando GraalVM. Por último, muestro cómo integrar el proyecto con Kubernetes Maven Plugin para publicar las imágenes de contenedor de la aplicación en Docker Hub y desplegarlas en Kubernetes.
Aplicación de ejemplo Quarkus 2
En esta sección os enseñaré a construir una aplicación muy sencilla que devolverá una cita aleatoria cada vez que se haga un a petición al endpoint /quotes/random
.
Arrancando el proyecto
Probablemente ya dispongas de Maven en tu entorno, en este caso, la forma más sencilla de iniciar el proyecto es ejecutando el siguiente comando:
mvn io.quarkus:quarkus-maven-plugin:2.1.1.Final:create \
-DprojectGroupId=com.marcnuri.demo \
-DprojectArtifactId=kubernetes-maven-plugin-quarkus \
-DclassName="com.marcnuri.demo.kmp.quote.QuoteResource" \
-Dextensions='quarkus-resteasy-jackson'
Si el comando se completa con éxito, podrás ver un nuevo directorio kubernetes-maven-plugin-quarkus
con un proyecto Maven inicial y soporte para Maven wrapper.
No obstante, si no dispones de Maven en tu entorno, o prefieres una interfaz gráfica interactive, puedes navegar a code.quarkus.io para customizar y descargar un proyecto Quarkus con tus requisitos.
Recursos del proyecto
La aplicación debe de devolver una cita aleatoria cada vez que un usuario haga una petición a un endpoint. Para ello necesitamos que la aplicación cargue las citas desde un fichero JSON situado en el directorio de recursos del proyecto. Para ello, debes de añadir el fichero quotes.json al directorio src/main/resources/quotes/
.
Endpoint
Una vez tenemos los recursos de la aplicación, podemos comenzar a implementar el código. El primer paso es crear un POJO Quote que utilizaremos para mapear la citas definidas en el fichero JSON cuando se deserialice.
1public class Quote implements Serializable {
2 /** ... **/
3 private String content;
4 private String author;
5 /** ... **/
6}
A continuación, debemos de crear una clase QuoteService que proporcionará el servicio para leer las citas desde el fichero y seleccionará una cita al azar.
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 var 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
utiliza Jackson para leer y deserializar el fichero quotes.json en una variable de clase ArrayList
que se utilizará posteriormente para escoger una cita al azar.
El método getRandomQuote
devuelve una entrada Quote
aleatoria de la ArrayList
para cada llamada.
Para finalizar la aplicación, necesitamos modificar el endpoint de muestra para que utilice el servicio que acabamos de implementar. Para ello, modificaremos 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 @Produces(MediaType.TEXT_PLAIN)
11 public Response getRandomQuote() {
12 final var randomQuote = quoteService.getRandomQuote();
13 return Response
14 .ok(randomQuote.getContent(), MediaType.TEXT_PLAIN_TYPE)
15 .header(HEADER_QUOTE_AUTHOR, randomQuote.getAuthor())
16 .build();
17 }
18
19 @Inject
20 public void setQuoteService(QuoteService quoteService) {
21 this.quoteService = quoteService;
22 }
23}
El método getRandomQuote
emplea una instancia de la clase QuoteService
(descrita anteriormente) para obtener una cita al azar y devolver su contenido en el cuerpo de la respuesta HTTP. Además, el autor de la cita también se devuelve como una cabecera de la respuesta.
Una vez completados todos los pasos, podemos arrancar la aplicación en modo desarrollo empleando el siguiente comando:
./mvnw clean compile quarkus:dev
[INFO] --- quarkus-maven-plugin:2.1.1.Final:dev (default-cli) @ kubernetes-maven-plugin-quarkus ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory D:\00-MN\projects\marcnuri-demo\kubernetes-maven-plugin-quarkus\src\test\resources
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 2 source files to D:\00-MN\projects\marcnuri-demo\kubernetes-maven-plugin-quarkus\target\test-classes
Listening for transport dt_socket at address: 5005
__ ____ __ _____ ___ __ ____ ______
--/ __ \/ / / / _ | / _ \/ //_/ / / / __/
-/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2021-07-04 07:49:33,690 INFO [io.quarkus] (Quarkus Main Thread) kubernetes-maven-plugin-quarkus 1.0.0-SNAPSHOT on JVM (powered by Quarkus 2.1.1.Final) started in 3.774s. Listening on: http://localhost:8080
2021-07-04 07:49:33,693 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2021-07-04 07:49:33,693 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, resteasy, resteasy-jackson, smallrye-context-propagation]
--
Tests paused, press [r] to resume, [h] for more options>
Si todo ha ido bien, el endpoint estará accesible en http://localhost:8080/quotes/random.
Compilar un ejecutable nativo con GraalVM
Ha llegado el momento de hacer que nuestra aplicación sea supersónica. Para ello vamos a utilizar GraalVM para crear un binario nativo de la aplicación.
Lo primero es adaptar el proyecto para que sea completamente compatible con GraalVM.
Incluir recursos
GraalVM no incluirá ninguno de los recursos disponibles en el classpath durante la compilación del binario utilizando native-image. Todos aquellos recursos que deban de estar disponibles en tiempo de ejecución, debemos de incluirlos específicamente cuando se crea la imagen.
Con Quarkus es muy sencillo configurar GraalVM. En este sentido, para que GraalVM tenga en cuenta nuestro fichero quotes.json
, tenemos que modificar el fichero application.properties del proyecto e incluir la siguiente línea:
1quarkus.native.additional-build-args=-H:IncludeResources=.*\.json$
Con esta instrucción le indicamos a Quarkus que añada la opción -H:IncludeResources
al comando native-image
. En este caso, queremos que se añada cualquier fichero que tenga la extensión .json
.
Reflection en la imagen nativa
La deserialización de JSON de Jackson utiliza reflection para crear las instancias de las clases de destino cuando realiza lecturas. La compilación mediante Graal native image necesita saber antes de tiempo qué elementos van a ser accedidos mediante reflection en tiempo de ejecución del programa.
Quarkus facilita esta configuración mediante la anotación @RegisterForReflection
que automatiza la tarea. En el caso de nuestra aplicación, anotaremos la clase Quote.
Construyendo la aplicación nativa
Una vez hemos adaptado la aplicación para que sea completamente compatible con GraalVM, podemos proceder a compilar la imagen en modo nativo. Si Graal VM con soporte para native-image está disponible en nuestro entorno, será tan sencillo como ejecutar el siguiente comando:
./mvnw clean package -Pnative
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildRunner] D:\00-MN\bin\graalvm-ce-java11-21.1.0\bin\native-image.cmd
-J-Djava.util.logging.manager=org.jboss.logmanager.LogManager -J-Dsun.nio.ch.maxUpdateArraySize=100
-J-Dvertx.logger-delegate-factory-class-name=io.quarkus.vertx.core.runtime.VertxLogDelegateFactory
-J-Dvertx.disableDnsResolver=true -J-Dio.netty.leakDetection.level=DISABLED -J-Dio.netty.allocator.maxOrder=3
-J-Duser.language=en -J-Duser.country=US -J-Dfile.encoding=UTF-8 -H:IncludeResources=.*.json\$
--initialize-at-build-time=
-H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy\$BySpaceAndTime -H:+JNI
-H:+AllowFoldMethods -jar kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner.jar -H:FallbackThreshold=0
-H:+ReportExceptionStackTraces -H:-AddAllCharsets -H:EnableURLProtocols=http -H:-UseServiceLoaderFeature
-H:+StackTrace kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner
[kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner:2772] classlist: 2,473.21 ms, 0.96 GB
[kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner:2772] (cap): 3,921.54 ms, 0.96 GB
[kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner:2772] setup: 6,147.99 ms, 0.96 GB
08:52:27,268 INFO [org.jbo.threads] JBoss Threads version 3.4.0.Final
[kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner:2772] (clinit): 664.62 ms, 4.67 GB
[kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner:2772] (typeflow): 16,865.10 ms, 4.67 GB
[kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner:2772] (objects): 20,902.79 ms, 4.67 GB
[kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner:2772] (features): 945.86 ms, 4.67 GB
[kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner:2772] analysis: 40,710.23 ms, 4.67 GB
[kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner:2772] universe: 1,702.71 ms, 4.67 GB
[kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner:2772] (parse): 4,527.99 ms, 4.67 GB
[kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner:2772] (inline): 8,267.27 ms, 5.83 GB
[kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner:2772] (compile): 24,709.00 ms, 5.69 GB
[kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner:2772] compile: 39,765.47 ms, 5.69 GB
[kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner:2772] image: 4,563.42 ms, 5.69 GB
[kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner:2772] write: 2,281.61 ms, 5.69 GB
# Printing build artifacts to: kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner.build_artifacts.txt
[kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner:2772] [total]: 98,272.86 ms, 5.69 GB
[WARNING] [io.quarkus.deployment.pkg.steps.NativeImageBuildRunner] objcopy executable not found in PATH. Debug symbols will not be separated from executable.
[WARNING] [io.quarkus.deployment.pkg.steps.NativeImageBuildRunner] That will result in a larger native image with debug symbols embedded in it.
[INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in 106997ms
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 02:04 min
[INFO] Finished at: 2021-07-04T08:53:44+02:00
[INFO] -----------------------------------------------------------------------
Si la ejecución termina con éxito, un nuevo binario kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner
estará disponible enel directorio target.
Ahora podemos ejecutar la aplicación de forma nativa mediante el siguiente comando:
./target/kubernetes-maven-plugin-quarkus-1.0.0-SNAPSHOT-runner
Del mismo modo que cuando ejecutamos la aplicación en modo JVM, el endpoint está disponible en http://localhost:8080/quotes/random.
Si GraalVM no está disponible en nuestro sistema, pero sí disponemos de Docker, el mismo comando puede ejecutarse dentro de un Contenedor Docker para construir un binario nativo de Linux:
./mvnw clean package -Pnative -Dquarkus.native.container-build=true
Integración con Kubernetes Maven Plugin (Eclipse JKube)
Este es el último paso del tutorial. En esta sección, os enseñaré cómo integrar el proyecto con Kubernetes Maven Plugin (Eclipse JKube).
El proceso es tan sencillo como añadir Kubernetes Maven Plugin al pom.xml de nuestro proyecto.
1<properties>
2 <!-- ... -->
3 <jkube.version>1.17.0</jkube.version>
4 </properties>
5 <!-- ... -->
6 <build>
7 <!-- ... -->
8 <plugins>
9 <!-- ... -->
10 <plugin>
11 <groupId>org.eclipse.jkube</groupId>
12 <artifactId>kubernetes-maven-plugin</artifactId>
13 <version>${jkube.version}</version>
14 </plugin>
15 </plugins>
16 </build>
La configuración se reduce a añadir un elemento <plugin>
con groupId
, artifactId
&version
para indicar que queremos usar Kubernetes Maven Plugin. En muchos casos, esto será suficiente, ya que el plugin tiene un modo Zero-Config que se ocupa de definir la mayoría de opciones por nosotros analizando el proyecto e infiriendo los valores recomendados para la configuración.
Construir la imagen de contenedor (Docker) (k8s:build)
EL primero paso es eliminar los ficheros Dockerfile que Quarkus incluye en el directorio src/main/docker
. Kubernetes Maven Plugin detecta toda la confiugración de forma automática, así que no es necesario mantener estos ficheros y los podemos eliminar de forma segura.
El modo Zero-Config de Eclipse JKube emplea Generators y Enrichers para ofrecer los valores de configuración inferidos del análisis del proyecto. Para este proyecto, toda la configuración se obtiene de forma automática salvo el ajuste de la autorización para el registro de Docker Hub. En este caso, vamos a configurar JKube para que lea las credenciales push de las variables de entorno DOCKER_HUB_USER
andDOCKER_HUB_PASSWORD
.
Para ello tienes que añadir los siguiente en la configuración del plugin.
1<plugin>
2 <groupId>org.eclipse.jkube</groupId>
3 <artifactId>kubernetes-maven-plugin</artifactId>
4 <version>${jkube.version}</version>
5 <configuration>
6 <authConfig>
7 <push>
8 <username>${env.DOCKER_HUB_USER}</username>
9 <password>${env.DOCKER_HUB_PASSWORD}</password>
10 </push>
11 </authConfig>
12 </configuration>
13</plugin>
Ya que hemos desarrollado el proyecto con soporte tanto para Quarkus en modo JVM como en modo nativo, vamos a generar 2 imágenes (Docker) de contenedor diferentes dependiendo en el perfil de Maven que seleccionemos.
Imagen Docker ejecutando la aplicación con JVM
fast-jar
es el modo por defecto de empaquetado para el modo JVM en Quarkus 2. Esto implica que a menos que se indique otra opción (e.g. Maven Profile), el comando mvn package
generará los ficheros y artefactos necesarios para este modo. Como voy a publicar las imágenes en Docker Hub, esta imagen la nombraré marcnuri/kubernetes-maven-plugin-quarkus:jvm
.
Esto creará una imagen para el repositorio marcnuri con el nombre kubernetes-maven-plugin-quarkus y el tag jvm
.
Para modificar el valor por defecto que JKube infiere para nuestro proyecto Quarkus, tenemos que modificar el valor de la propiedad de maven jkube.generator.name
. Esto lo podemos hacer añadiendo una entrada a las propiedades globales del proyecto en el fichero pom.xml
:
1<properties>
2 <!-- ... -->
3 <jkube.version>1.17.0</jkube.version>
4 <jkube.generator.name>marcnuri/kubernetes-maven-plugin-quarkus:jvm</jkube.generator.name>
5 </properties>
Ahora podemos ejecutar el siguiente comando para construir la imagen de Docker:
./mvnw clean package k8s:build
[INFO] --- kubernetes-maven-plugin:1.17.0:build (default-cli) @ kubernetes-maven-plugin-quarkus ---
[INFO] k8s: Running in Kubernetes mode
[INFO] k8s: Building Docker image in Kubernetes mode
[INFO] k8s: Running generator quarkus
[INFO] k8s: quarkus: Using Docker image quay.io/jkube/jkube-java-binary-s2i:0.0.9 as base / builder
[INFO] k8s: [marcnuri/kubernetes-maven-plugin-quarkus:jvm] "quarkus": Created docker-build.tar in 7 seconds
[INFO] k8s: [marcnuri/kubernetes-maven-plugin-quarkus:jvm] "quarkus": Built image sha256:4947d
[INFO] k8s: [marcnuri/kubernetes-maven-plugin-quarkus:jvm] "quarkus": Tag with latest
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 16.113 s
[INFO] Finished at: 2021-07-04T16:22:27+02:00
[INFO] ------------------------------------------------------------------------
Imagen Docker ejecutando la aplicación en modo nativo
El proceso para el modo nativo es muy similar al que seguimos en el paso anterior para el modo JVM. En este caso, lo que quiero es generar una imagen con el siguiente nombre: marcnuri/kubernetes-maven-plugin-quarkus:native
.
Como el proyecto ya contiene un perfil de Maven para el modo nativo, conseguir esto será tan sencillo como sobreescribir la propiedad global jkube.generator.name
para el perfil native
.
1<profiles>
2 <profile>
3 <id>native</id>
4 <!-- ... -->
5 <properties>
6 <quarkus.package.type>native</quarkus.package.type>
7 <jkube.generator.name>marcnuri/kubernetes-maven-plugin-quarkus:native</jkube.generator.name>
8 </properties>
9 </profile>
10</profiles>
Ahora podemos ejecutar el siguiente comando para construir la imagen nativa de Docker:
./mvnw clean package k8s:build -Pnative
[INFO] --- kubernetes-maven-plugin:1.17.0:build (default-cli) @ kubernetes-maven-plugin-quarkus ---
[INFO] k8s: Running in Kubernetes mode
[INFO] k8s: Building Docker image in Kubernetes mode
[INFO] k8s: Running generator quarkus
[INFO] k8s: quarkus: Using Docker image registry.access.redhat.com/ubi8/ubi-minimal:8.1 as base / builder
[INFO] k8s: Pulling from ubi8/ubi-minimal
[INFO] k8s: Digest: sha256:df6f9e5d689e4a0b295ff12abc6e2ae2932a1f3e479ae1124ab76cf40c3a8cdd
[INFO] k8s: Status: Downloaded newer image for registry.access.redhat.com/ubi8/ubi-minimal:8.1
[INFO] k8s: Pulled registry.access.redhat.com/ubi8/ubi-minimal:8.1 in 3 seconds
[INFO] k8s: [marcnuri/kubernetes-maven-plugin-quarkus:native] "quarkus": Created docker-build.tar in 498 milliseconds
[INFO] k8s: [marcnuri/kubernetes-maven-plugin-quarkus:native] "quarkus": Built image sha256:5a1d5
[INFO] k8s: [marcnuri/kubernetes-maven-plugin-quarkus:native] "quarkus": Tag with latest
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 25.851 s
[INFO] Finished at: 2021-07-04T15:10:31Z
[INFO] ------------------------------------------------------------------------
Publicar la imagen en Docker Hub (k8s:push)
Independientemente del empaquetado que elijamos (JVM o native), publicar la imagen en Docker Hub es tan sencillo como ejecutar el siguiente comando (siempre y cuando las variables de entorno requeridas estén disponibles):
./mvnw k8s:push
# or if a native image was built
./mvnw k8s:push -Pnative
Obviamente, esto tiene sentido desde una pipeline de integración continua. Si estás ejecutando este código desde un entorno local seguro, puedes establecer las credenciales de esta forma:
./mvnw k8s:push -Djkube.docker.push.username=$username -Djkube.docker.push.password=$password
# or if a native image was built
./mvnw k8s:push -Pnative -Djkube.docker.push.username=$username -Djkube.docker.push.password=$password
En mi caso, estoy ejecutando esto desde un GitHub Actions Workflow:
Desplegando la aplicación en Kubernetes (k8s:apply)
Ahora que la imagen se encuentra disponible en Docker Hub, puedo desplegar la aplicación en mi cluster de Kubernetes.
La principal ventaja de JKube es que no hay que lidiar con ficheros de configuración YAML. El plugin se encarga de generar todo lo necesario, así que únicamente hay que ejecutar lo siguiente:
./mvnw k8s:resource k8s:apply
# or if a native image was built
./mvnw k8s:resource k8s:apply -Pnative
[INFO] --- kubernetes-maven-plugin:1.17.0:apply (default-cli) @ kubernetes-maven-plugin-quarkus ---
[INFO] k8s: Using Kubernetes at https://192.168.99.120:8443/ in namespace default with manifest D:\00-MN\projects\marcnuri-demo\kubernetes-maven-plugin-quarkus\target\classes\META-INF\jkube\kubernetes.yml
[INFO] k8s: Creating a Service from kubernetes.yml namespace default name kubernetes-maven-plugin-quarkus
[INFO] k8s: Created Service: target\jkube\applyJson\default\service-kubernetes-maven-plugin-quarkus-4.json
[INFO] k8s: Creating a Deployment from kubernetes.yml namespace default name kubernetes-maven-plugin-quarkus
[INFO] k8s: Created Deployment: target\jkube\applyJson\default\deployment-kubernetes-maven-plugin-quarkus-4.json
[INFO] k8s: HINT: Use the command `kubectl get pods -w` to watch your pods start up
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 8.678 s
[INFO] Finished at: 2021-07-06T06:13:35+02:00
[INFO] ------------------------------------------------------------------------
Este es un proyecto muy sencillo, así que no he creado ningún Ingress para exponer el servicio. Esto significa que la aplicación se encuentra inaccesible. En un próximo post explicaré como crear dicho Ingress. En todo caso, si estás ejecutando la aplicación en Minikube y quieres acceder a la misma, con JKube sería tan sencillo como ejecutar:
./mvnw k8s:resource k8s:apply -Djkube.enricher.jkube-service.type=NodePort
# or if a native image was built
./mvnw k8s:resource k8s:apply -Djkube.enricher.jkube-service.type=NodePort -Pnative
minikube service kubernetes-maven-plugin-quarkus
Si todo ha ido bien, una ventana de navegador se abrirá y podrás ver una página como esta:
Conclusión
En esta publicación os he mostrado como desarrollar una aplicación Quarkus 2 muy sencilla, crear una imagen nativa mediante GraalVM e integrarla con Kubernetes Maven Plugin. En la primera sección, he demostrado como generar la aplicación básica y crear un endpoint REST que devuelve una cita al azar para cada petición. A continuación os he mostrado como configurar la aplicación para que sea compatible con GraalVM y así poder generar un binario nativo. Por último, os he enseñado como configurar Kubernetes Maven Plugin para poder construir la imagen de contenedor y publicarla en el registro de Docker Hub. Además, hemos visto lo sencillo que es desplegar la aplicación en Kubernetes a través de Eclipse JKube.
El código fuente completo de este artículo se encuentra en GitHub.