Using Quarkus — the Kubernetes Native Java stack

Grzegorz Smolko
AI+ Enterprise Engineering
12 min readJun 28, 2020

Learn how to run your standard JAX-RS microservice on Quarkus

In this article we will show steps needed in order to run your Java JAX-RS based microservice in the Quarkus runtime and show differences to other popular microservices runtime — IBM Open Liberty.

Quarkus is a new Java stack optimized to run Java microservice applications in Kubernetes. It is Eclipse MicroProfile compatible, but it doesn’t implement full Java EE/Jakarta EE stack unlike Open Liberty. If your application is currently utilizing many Jakarta EE features, Open Liberty will be a much better fit.

Quarkus runs on OpenJDK or GraalVM. The main benefit is that GraalVM provides platform native compilation so that the resulting microservice will not require a JVM to run, as the application will be provided as a small executable instead. Native compilation reduces size of the container image and startup times. This opens up new possibilities for Java platform, as you can now run your Java code as serverless.

This native compilation however is quite lengthy process that requires significant amount of resources and does introduce some limitations, which we will also be presenting as we demonstrate how to run the “IBM StockQuote” microservice in Quarkus.

How to start?

The StockQuote microservice is part of the Stock Trader application mentioned before. It is currently built using Maven, if your application is not currently built using Maven, first step would be to change the build process to utilize that (this is beyond scope of this article).

There are several articles discussing how to run StockQuote service in Open Liberty, so here we will mostly focus on running it in Quarkus runtime.

Both runtimes are composable and allow you to select features you would like to use.

The Quarkus application generator allows you to start quickly and generate stub for your application, based on selected extensions:

… but what extensions you should select?

You will need to know what kind of the MicroProfile APIs your application is currently using. One hint could be the application pom.xml file — look for entries with groupId stating with org.eclipse.microprofile.* in the dependenciessection, similar to these :

<groupId>org.eclipse.microprofile.health</groupId>
...
<groupId>org.eclipse.microprofile.metrics</groupId>
...

you may also have dependency defining the entire MicroProfile stack like this:

<dependency>
<groupId>org.eclipse.microprofile</groupId>
<artifactId>microprofile</artifactId>
<version>3.2</version>
<type>pom</type>
<scope>provided</scope>
</dependency>

In case of Open Liberty it is much simpler, as it provides umbrella feature called microProfile-3.2, which enables all MicroProfile related APIs, but also gives you flexibility to enable each API separately.

Here is our mapping table that shows Open Liberty features and Quarkus extensions required by StockQuote microservice:

and our final list of selected extensions is:

Finally, generate the application and save the downloaded archive to your disk.

Extract the archive and copy pom.xml, src\main\docker folder, and src\main\resources folder to your application directory structure. In our case we used the generated pom.xml file as a new base and added missing dependencies (jedis, junit) to it, but if your pom file is more complex, you may do it the other way around and copy the dependencies from the generated file.

The docker folder contains basic docker files that allow you to build a container with Quarkus and your application in two versions — traditional JVM or native.

The resources folder contains an application.properties file, which configures your Quarkus application.

To start testing your application in the development mode type:

mvnw compile quarkus:dev

If your application starts successfully you should see output similar to this (some lines removed to make it shorter):

[INFO] Scanning for projects...
[INFO]
[INFO] --------------------< com.stocktrader:stock-quote >---------------------
[INFO] Building stock-quote 1.0.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ stock-quote ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 4 resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ stock-quote ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- quarkus-maven-plugin:1.3.2.Final:dev (default-cli) @ stock-quote ---
Listening for transport dt_socket at address: 5005
__ ____ __ _____ ___ __ ____ ______
--/ __ \/ / / / _ | / _ \/ //_/ / / / __/
-/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2020-04-30 14:18:22,933 DEBUG [io.qua.ely.sec.pro.dep.ElytronPropertiesProcessor] (build-42) Configuring from PropertiesRealmConfig, users=users.properties, roles=roles.properties
2020-04-30 14:18:24,885 INFO [io.qua.arc.pro.BeanProcessor] (build-26) Found unrecommended usage of private members (use package-private instead) in application beans:
- @Inject field com.ibm.hybrid.cloud.sample.stocktrader.stockquote.StockQuote#apiConnectClient,
- @Inject field com.ibm.hybrid.cloud.sample.stocktrader.stockquote.StockQuote#iexClient
2020-04-30 14:18:25,258 INFO [com.ibm.hyb.clo.sam.sto.sto.StockQuote] (main) API Connect URL not found from env var from config map, so defaulting to value in jvm.options: null
...
2020-04-30 14:18:26,204 INFO [io.quarkus] (main) stock-quote 1.0.0-SNAPSHOT (powered by Quarkus 1.3.2.Final) started in 4.421s. Listening on: http://0.0.0.0:8080
2020-04-30 14:18:26,204 INFO [io.quarkus] (main) Profile dev activated. Live Coding activated.
2020-04-30 14:18:26,205 INFO [io.quarkus] (main) Installed features: [cdi, jaeger, kubernetes, mutiny, rest-client, resteasy, resteasy-jsonb, security, security-properties-file, smallrye-context-propagation, smallrye-fault-tolerance, smallrye-health, smallrye-jwt, smallrye-metrics, smallrye-openapi, smallrye-opentracing, swagger-ui, vertx, vertx-web]

To run your application in the native mode type:

mvnw package -Pnative

which produces native application that you can execute by invoking: ./target/stock-quote-1.0.0-SNAPSHOT-runner

However building native application locally requires complex setup as described here.

Instead, we decided to utilize a multi-stage Docker build to build and run the service in the docker container. For that we created specialized dockerfile — src/main/docker/Dockerfile.multistage and built application with the following command:

docker build -f src/main/docker/Dockerfile.multistage -t quarkus-stock-quote-native .

And then run:

docker run --rm -p 8080:8080 --name stock-quote -e REDIS_URL=redis://172.17.0.2:6379 quarkus-stock-quote-native

This approach also allowed us to build the service on Windows and then execute it in the Linux based container. For details on how to configure the Docker Desktop for Windows check this.

StockQuote issues when run in Quarkus

The application compiled fine, without any code changes, but we hit several issues during testing. Quarkus is not Jakarta EE compliant like Open Liberty and have some limitations, especially when run in native mode.

Application context root and metrics incompatibilities

By default context-root of the Quarkus application is set to / and in the Liberty it was /stock-quote. It is possible to change that for the Quarkus application via a property quarkus.servlet.context-path=/stock-quote specified in the application.properties, but that breaks the metrics URI, which now becomes /stock-quote/metrics instead of /metrics, which may break your container orchestrator expectations.

Our solution was to change contex-root to / and change StockQuote service path to /stock-quote using a class level annotation like this:

@Path("/stock-quote")

Constructors, static blocks, @PostConstruct methods and @ConfigProperty variables evaluated and compiled at build time

If you plan to run Quarkus application in native mode, you have to be aware that static blocks, constructors, methods with @PostConstruct and variables with @PropertyConfig are evaluated and compiled to native code at build time. This unfortunately prevents you from using these methods to initialize your application during runtime.

The StockQuote service was using a static block and also constructor to read environment variables to initialize itself. Our solution was to use lazy initialization, move code from static block and constructor to new initializeApp() method and remove the constructor and the static block completely, like this:

private void initializeApp() {
if(initialized) {
logger.info("Application already intitialized");
return ;
}
initialized = true;
logger.info("The application is initializing...");
// code from static block and constructor
...
}

and then simply call this method from all our service methods.

More elegant, but proprietary to the Quarkus runtime, solution would be to use Observable and StartupEvent as described in Quarkus application initialization and change our method to:

private void initializeApp(@Observes StartupEvent ev) {
logger.info("The application is initializing...");
// code from static block and constructor
...
}

JMX API is not available

JMX API is not available in native Quarkus mode. In the StockQuote, Redis client library (Jedis) was using JMX. We needed to change JedisPool configuration not to use JMX in the following way:

//### Quarkus - disable jmx in jedis
JedisPoolConfig jedisConfiguration = new JedisPoolConfig();
jedisConfiguration.setJmxEnabled(false);
logger.info("Initializing JedisPool");
jedisPool = new JedisPool(jedisConfiguration, jedisURI);

Reflection API limitations

In the native mode Quarkus has several limitations with the Reflection API, which are described in Reflection on Substrate VM and Quarkus — Tips for writing native applications articles, this might prevent your application from running successfully.

In the StockQuote, the Reflection API is used ‘behind the scenes’ by the Jedis library and results in the following exception during runtime:

Quarkus is not aware of additional classes loaded by that library by Class.forName() method. To solve that we need to create a reflection-config.json file in the /src/main/resources folder that specifies additional classes that should be considered during build. The file has following content:

[
{
"name" : "org.apache.commons.pool2.impl.DefaultEvictionPolicy",
"allDeclaredConstructors" : true,
"allPublicConstructors" : true,
"allDeclaredMethods" : true,
"allPublicMethods" : true,
"allDeclaredFields" : true,
"allPublicFields" : true
}
]

and add a reference to it in application.properties file:

# Reflection Configuration
quarkus.native.additional-build-args =-H:ReflectionConfigurationFiles=reflection-config.json

After all these changes the application builds and runs successfully in both JVM and native mode.

Alternative caching configuration

In the StockQuote Redis is used as a caching service to store quotes received from the Internet. It also shows how to integrate with remote services.

If you would only be interested in an internal data caching, Quarkus offers its own cache extension, which you can very easy integrate in your service.

You need to add the following to the pom.xml:

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-cache</artifactId> </dependency>

or use Quarkus Maven extension plugin and issue:

mvnw quarkus:add-extention -Dextensions="quarkus-cache"

and annotate the service method like this:

@CacheResult(cacheName = "quote")
public Quote getStockQuote(@PathParam("symbol") String symbol) throws IOException {

and get rid of all the code that calls Redis. Sample implementation is done in the quarkus-cache branch

Quarkus Security configuration

The StockQuote service can work with a basic security using embedded user registry or utilizing JWT authentication. For smoke testing, we used basic configuration, for final deployment to OpenShift and integration of other IBM Stock Trader components we used JWT authentication. Here we will only talk about JWT configuration, you can look in the Github repo for details about file based basic security configuration.

Enabling security in Quarkus

In the Open Liberty, security constraints are configured in the web.xml and/or via annotations in the Java code. Annotations are still valid in Quarkus, but it lacks web.xml and this part must be done via the application.properties file. For more details look into Quarkus — Security Guide.

First, you need to ensure that smallrye-jwt extension is enabled in your pom.xml:

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>

or use Quarkus Maven extension plugin and issue:

mvnw quarkus:add-extention -Dextensions="quarkus-smallrye-jwt"

Then configure jwt properties. In our configuration the application.properties file looks like this:

# Security configuration - jwt
quarkus.smallrye-jwt.enabled=true
mp.jwt.verify.publickey.location=default-cert.pem
mp.jwt.verify.issuer=http://stock-trader.ibm.com
# Security constraints
quarkus.http.auth.basic=true
quarkus.http.auth.permission.authenticated.paths=/*
quarkus.http.auth.permission.authenticated.policy=authenticated

where:

  • quarkus.smallrye-jwt.enabled=true — enables jwt extension
  • mp.jwt.verify.publickey.location=default-cert.pem — specifies the file with certificate used to validate token
  • mp.jwt.verify.issuer=http://stock-trader.ibm.com — specifies the token issuer

Optionally, if you want to validate audiences, you need to add the following line:

smallrye.jwt.verify.aud=stock-trader

For security constraints, we used simple configuration to just protect whole application for any authenticated user.

Deployment to OpenShift

Quarkus applications can be deployed to an OpenShift cluster in a few ways:

  • using manifests (yaml files)
  • using s2i tooling (via oc new-app)
  • using Quarkus OpenShift extension (via mvn deploy)

In this article we are discussing the first option, that uses manifests, as it provides the most flexibility, and can be easily integrated in your current DevOps pipelines.

Deploying to OpenShift using manifests — BuildConfig, ImageStreams and Deployment

In this article we assume, that application sources are available to OpenShift cluster via code repository. In our case sources are located in this Github repo in quarkus branch.

Quarkus provides images that can be used to build an application directly on the OpenShift. We will utilize this method to compile and build an application directly on the target cluster. In a real case scenario, it should be your development cluster, not the production one, as the build process is using lots of resources.

Create ImageStreams that will hold build tools and application images:

Create a BuildConfig definition that will tell OpenShift how to process the application. The bc-stock-quote-quarkus.yaml is build config that is using Git and Docker strategy. The config pulls the application sources from the specified git repository and branch:

source:
type: Git
git:
uri: 'https://github.com/gasgithub/stock-quote.git'
ref: quarkus

It is using a Dockerfile located in the root of the project, and the source image stream that we specified in previous step:

strategy:
type: Docker
dockerStrategy:
from:
kind: ImageStreamTag
name: 'centos-quarkus-maven:19.3.1-java11'

As output, it produces a new image stream with the application:

output:
to:
kind: ImageStreamTag
name: 'stock-quote-quarkus:latest'

We could already deploy and run this image using depl-stock-quote-quarkus.yaml, however this image is quite big (920MB) as it contains lots of tools used during build. It is recommended to create a minimal image that will only contain the application.

To achieve this we created two additional image streams:

We also need to create another build config that will build the small target image (70MB) — bc-minimal-stock-quote-quarkus.yaml. This build config is using the previously built image and an inline dockerfile to copy built application and run it:

source:
type: Dockerfile
dockerfile: |-
FROM registry.access.redhat.com/ubi8/ubi-minimal:latest
COPY application /application
CMD /application
EXPOSE 8080
images:
- from:
kind: ImageStreamTag
name: 'stock-quote-quarkus:latest'
as: null
paths:
- sourcePath: /usr/src/app/target/application
destinationDir: .

The application is deployed using a yaml file, that defines a deployment, a service and a route — depl-minimal-stock-quote-quarkus.yaml.

To deploy all these resources to OpenShift issue the following commands:

oc new-project stock-quote-quarkus
oc apply -f is-quarkus-maven.yaml
oc apply -f is-stock-quote-quarkus.yaml
oc apply -f bc-stock-quote-quarkus.yaml
oc apply -f is-ubi-minimal.yaml
oc apply -f is-minimal-stock-quote-quarkus.yaml
oc apply -f bc-minimal-stock-quote-quarkus.yaml
oc apply -f depl-minimal-stock-quote-quarkus.yaml

We could easily integrate our build config with the Github via a web hook, that would automatically build and deploy the new application whenever its code is changed, however for now, you have to trigger the build manually via console or command line:

oc start-build stock-quote-quarkus

The build is also automatically triggered whenever one of the source images used in image streams is changed, for example due to security updates.

Now, check if your build is successful:

$ oc get builds
NAME TYPE FROM STATUS STARTED DURATION
stock-quote-quarkus-4 Docker Git@ab4b543 Complete 12 minutes ago 10m14s
minimal-stock-quote-quarkus-4 Docker Dockerfile Complete 2 minutes ago 1m15s

As you can see ‘minimal’ build was also automatically ran as the source image for that has changed.

Our service is accessible via url similar to: http://minimal-stock-quote-quarkus-NAMESPACE.CLUSTER-URL/stock-quote/IBM however we will not be able to test it directly as it is protected via JWT authorization. For manual testing, you can use file based security configuration and disable JWT.

Overriding application configuration settings in OpenShift

By default, Quarkus application configuration is stored in the application.properties file and is processed during build time. Very often you would like to override these values during deployment. It can be done via environment variables, but you have to consult documentation for the extensions that you are using, as not all properties are changeable in the runtime.

In our service we overrode settings for Redis and JWT configuration, specifying the following in deployment file:

          env:
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: redis
key: redis.url
- name: MP_JWT_VERIFY_PUBLICKEY
valueFrom:
configMapKeyRef:
name: jwt-config
key: jwt-ca.crt
- name: MP_JWT_VERIFY_ISSUER
valueFrom:
configMapKeyRef:
name: jwt-config
key: mp.jwt.verify.issuer
- name: SMALLRYE_JWT_VERIFY_AUD
valueFrom:
configMapKeyRef:
name: jwt-config
key: smallrye.jwt.verify.aud

What next?

Next step would be to make your service true serverless, by allowing it to scale to zero, utilizing KNative features of OpenShift, but it is a theme for another story….

--

--