Featured image for automating JBoss Web Server deployments with Ansible.

For microservices running in container environments, Java developers tend to want a self-contained image that incorporates the complete runtime environment needed to run an application. At the same time, developers want a minimally sized image for both efficiency and security. A bootable JAR can meet these requirements. This article describes how to create a bootable JAR using Red Hat JBoss Enterprise Application Platform (JBoss EAP) and Jakarta EE and incorporate useful extensions, particularly a PostgreSQL database and MicroProfile capabilities.

About the example application

For this demonstration, I have updated the application used in my previous series of articles. Like that series, this article runs the application on an instance of Red Hat OpenShift Container Platform, an open source Platform-as-a-Service (PaaS) based on Kubernetes. One of my priorities for all of these demonstrations is to optimize the consumption of resources that are the main factors in the costs charged by a PaaS vendor: image size, memory use, and CPU use.

The bootable JAR is a feature of WildFly, a lightweight runtime for building Java applications. We'll use JBoss EAP, a fully supported enterprise Java platform based on WildFly. We'll provision custom layers that expand the application's capabilities using Galleon feature-packs.

The source code for this application is available on my GitHub repository. To track the application's evolution, I created a new tag named Galleon_Runtime_EAP_XP_bootable_jar_version for the version used in this article. You can use the tags assigned to repository branches to analyze the configuration information through various iterations.

Prerequisites

You will need the following software to execute the example application:

Note: If you prefer, you could use Red Hat CodeReady Containers to run an OpenShift instance on your local system.

Get the source code

To install the source code from my GitHub repository, open a terminal, select a folder, and clone the repository using the following command:

$ git clone https://github.com/mvocale/JBoss_EAP_cloud_ready.git

Now, change into the directory for the project. Check out the Galleon_Runtime_EAP_XP_bootable_jar_version version, where I stored the code used to implement the bootable JAR running mode, using the following command:

$ git checkout tags/Galleon_Runtime_EAP_XP_bootable_jar_version

The weather-app-eap-cloud-ready subdirectory contains the main application, copied from an earlier version of the same application. The source code uses the Jakarta EE 8 and MicroProfile 3 specifications on top of JBoss EAP XP 3 in bootable JAR mode, employing Galleon to install only the required subsystems. The final container image was improved using the runtime version of OpenJDK 11.

The application needs a database to store information, so I chose PostgreSQL. The postgresql-database-layer subdirectory of the repository loads and configures the database. I could have simply used a feature-pack from the wildfly-datasources-galleon-pack project, which supports PostgreSQL as well as Microsoft SQL Server and Oracle. But one goal of this article is to show how to customize a JBoss EAP subsystem with features not supported by default, or how to change the behavior of a feature through a custom layer.

A shell script named deploy-openshift.sh contains all the instructions needed to install all the implemented components on top of OpenShift. If you don't want to perform every single step described, you can establish a connection to CodeReady Containers or an OpenShift remote cluster and run the script there.

Set up your environment

Now it's time to connect to OpenShift to deploy all the components needed by our application. If you are using CodeReady Containers, start it and log in as a developer using the following commands:

$ crc start
$ oc login -u developer -p developer https://api.crc.testing:6443

Otherwise, log into your OpenShift environment as follows, substituting your own appropriate values for $token and $server_url:

$ oc login --token=$token --server=$server_url

Create the project that will host the application:

$ oc new-project redhat-jboss-eap-cloud-ready-demo --display-name="Red Hat JBoss EAP Cloud Ready Demo"

Create and configure the PostgreSQL database:

# Import image related to Postgresql Database
$ oc import-image rhel8/postgresql-13:1-21 --from=registry.redhat.io/rhel8/postgresql-13:1-21 --confirm 
# Create the Postgresql Database Application
$ oc new-app -e POSTGRESQL_USER=mauro \
   -e POSTGRESQL_PASSWORD=secret \
   -e POSTGRESQL_DATABASE=weather postgresql-13:1-21 \
   --name=weather-postgresql

Add the PostgreSQL icon to the database:

$ oc patch dc weather-postgresql --patch '{"metadata": { "labels": { "app.openshift.io/runtime": "postgresql" } } }'

Now, deploy a set of actors that will help you get the benefits of MicroProfile specifications:

To install these projects, switch to the weather-app-eap-cloud-ready directory that hosts the application source code and run the following:

# Import Jaeger image from catalog
$ oc import-image distributed-tracing/jaeger-all-in-one-rhel8:1.24.1-1 --from=registry.redhat.io/distributed-tracing/jaeger-all-in-one-rhel8:1.24.1-1 --confirm
# Create the Jaeger application
$ oc new-app -i jaeger-all-in-one-rhel8:1.24.1-1
# Expose the route in order to make the Jaeger application available outside of OpenShift
$ oc expose svc jaeger-all-in-one-rhel8 --port=16686

# Create the Prometheus environment used to collect the values provided by MicroProfile Metrics specifications. Import the Prometheus image from catalog
$ oc import-image openshift4/ose-prometheus:v4.8.0-202110011559.p0.git.f3beb88.assembly.stream --from=registry.redhat.io/openshift4/ose-prometheus:v4.8.0-202110011559.p0.git.f3beb88.assembly.stream --confirm
# Create the config map with the Prometheus configurations
$ oc create configmap prometheus --from-file=k8s/prometheus.yml
### Create the Prometheus application
$ oc create -f k8s/ose-prometheus.yaml

# Create the Grafana environment used to collect the values provided by MicroProfile Metrics specifications. Import Grafana image from catalog
$ oc import-image openshift4/ose-grafana:v4.8.0-202110011559.p0.git.b987e4b.assembly.stream --from=registry.redhat.io/openshift4/ose-grafana:v4.8.0-202110011559.p0.git.b987e4b.assembly.stream --confirm
# Create the config map with the Grafana configurations
$ oc create configmap grafana --from-file=k8s/datasource-prometheus.yaml --from-file=k8s/grafana-dashboard.yaml --from-file=k8s/jboss_eap_grafana_dashboard.json
# Create the Grafana application
$ oc create -f k8s/ose-grafana.yaml

Build a custom layer for JBoss EAP XP

The version of the application created in my previous articles used Source-to-Image (S2I) to define and configure the JDBC driver and data source used by the application (see the installation shell script for details). I had a folder named extensions where I defined:

  • The module.xml file and JAR file used by the JBoss EAP module
  • The drivers.env file, where I specified the driver properties
  • The install.sh file to instruct the S2I build where to find the resources needed to configure the JBoss EAP driver and data source

The postgresql-database-layer subproject builds all the resources needed to provision and set up the JDBC driver and the data source subsystem. Let's change into this subproject:

$ cd postgresql-database-layer

This project creates a layer to provision and configure the driver module and the data source subsystem that manages the JDBC connection to the PostgreSQL database. Under the src/main/resources directory are two subdirectories:

  • layers/standalone: Contains the files needed to configure the driver (postgresql-driver) and the data source (postgresql-datasource).
  • module/org/postgresql/main: Contains the file needed to manage the JBoss module used to interact with the PostgreSQL database.

In the postgresql-driver directory lies a layer-spec.xml file that contains the parameters used by the Galleon framework to provision and configure the JDBC driver:

<?xml version="1.0" ?>
<layer-spec xmlns="urn:jboss:galleon:layer-spec:1.0" name="postgresql-driver">
	<feature spec="subsystem.datasources">
    	    <feature spec="subsystem.datasources.jdbc-driver">
             <param name="driver-name" value="postgresql"/>
        	  <param name="jdbc-driver" value="postgresql"/>
        	  <param name="driver-xa-datasource-class-name" value="org.postgresql.xa.PGXADataSource"/>
        	  <param name="driver-module-name" value="org.postgresql"/>
    	     </feature>
	</feature>
	<packages>
    	    <package name="org.postgresql"/>
	</packages>
</layer-spec>

Under the postgresql-datasource directory lies a layer-spec.xml file that contains the parameters used by the Galleon framework to provision and configure the data source:

<?xml version="1.0"?>
<layer-spec xmlns="urn:jboss:galleon:layer-spec:1.0" name="postgresql-datasource">
	<dependencies>
    	<layer name="postgresql-driver" />
	</dependencies>
	<feature spec="subsystem.datasources.data-source">
    	<param name="use-ccm" value="true" />
    	<param name="data-source" value="WeatherDS" />
    	<param name="enabled" value="true" />
    	<param name="use-java-context" value="true" />
    	<param name="jndi-name" value="${env.DB_JNDI}" />
    	<param name="connection-url" value="jdbc:postgresql://${env.WEATHER_POSTGRESQL_SERVICE_HOST}:${env.WEATHER_POSTGRESQL_SERVICE_PORT}/${env.DB_DATABASE}" />
    	<param name="driver-name" value="postgresql" />
    	<param name="user-name" value="${env.DB_USERNAME}" />
    	<param name="password" value="${env.DB_PASSWORD}" />
    	<param name="validate-on-match" value="false"/>
    	<param name="valid-connection-checker-class-name" value="org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLValidConnectionChecker"/>
    	<param name="exception-sorter-class-name" value="org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLExceptionSorter"/>
    	<param name="background-validation" value="true" />
    	<param name="background-validation-millis" value="60000" />
    	<param name="flush-strategy" value="IdleConnections" />
    	<param name="statistics-enabled" value="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:true}}" />
	</feature>
</layer-spec>

Finally, under the src/main/resources/modules/org/postgresql/main directory is a module.xml file that configures the JBoss module:

<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.0" name="org.postgresql">
    <resources>
   	 <artifact name="${org.postgresql:postgresql}"/>
    </resources>
    <dependencies>
   	 <module name="javax.api"/>
   	 <module name="javax.transaction.api"/>
    </dependencies>
</module>

The pom.xml file in the postgresql-database-layer directory builds the custom feature-pack using the Maven Galleon plug-in:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   ...
   <properties>
      <version.org.postgresql>42.2.18.redhat-00001</version.org.postgresql>
      <version.wildfly.galleon.pack>3.0.0.GA-redhat-00005</version.wildfly.galleon.pack>
      <version.wildfly.galleon.maven.plugin>5.2.4.Final</version.wildfly.galleon.maven.plugin>
   </properties>

   <dependencies>
      <!-- Import the Postgresql JDBC driver -->
      <dependency>
         <groupId>org.postgresql</groupId>
         <artifactId>postgresql</artifactId>
         <version>${version.org.postgresql}</version>
      </dependency>
      <dependency>
         <groupId>org.jboss.eap</groupId>
         <artifactId>wildfly-galleon-pack</artifactId>
         <version>${version.wildfly.galleon.pack}</version>
         <type>zip</type>
      </dependency>
   </dependencies>

   <build>
      <plugins>
         <plugin>
            <groupId>org.wildfly.galleon-plugins</groupId>
            <artifactId>wildfly-galleon-maven-plugin</artifactId>
            <version>${version.wildfly.galleon.maven.plugin}</version>
            <executions>
               <execution>
                  <id>wildfly-datasources-galleon-pack-build</id>
                  <goals>
                     <goal>build-user-feature-pack</goal>
                  </goals>
                  <phase>compile</phase>
                  <configuration>
                     <translate-to-fpl>true</translate-to-fpl>
                  </configuration>
               </execution>
            </executions>
         </plugin>
      </plugins>
   </build>
</project>

The build-user-feature-pack goal in the Galleon Maven plug-in builds the custom layers.

Build a JBoss EAP XP 3 bootable JAR

Unlike the example in my previous articles, this example does not put the application into a JBoss EAP XP container image as a deployment artifact. Instead, we create a bootable JAR containing a server, a packaged application, and the runtime required to launch the application. The source code remains the same.

Using a bootable JAR for deployment changes the following aspects of the previous example:

  • How we provision and configure database connectivity: We use the feature-pack implemented through the postgresql-database-layer subproject.
  • How we build the application: We use the wildfly-jar-maven-plugin Maven plug-in to create a bootable JAR.
  • How we build the application image: Instead of a JBoss EAP XP image, we use an OpenJDK image at build time and runtime, since we created a bootable JAR.

The wildfly-jar-maven-plugin plug-in is invoked in the pom.xml file in the weather-app-eap-cloud-ready subproject::

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   ...
   <build>
      <plugins>
         <plugin>
            <groupId>org.wildfly.plugins</groupId>
            <artifactId>wildfly-jar-maven-plugin</artifactId>
            <version>${bootable.jar.maven.plugin.version}</version>
            <configuration>
               <feature-packs>
                  <feature-pack>
                     <location>org.jboss.eap:wildfly-galleon-pack:${version.wildfly.galleon.pack}</location>
                  </feature-pack>
                  <feature-pack>
                     <groupId>com.redhat.examples</groupId>
                     <artifactId>postgresql-layer</artifactId>
                     <version>1.0.0</version>
                  </feature-pack>
               </feature-packs>
               <layers>
                  <layer>jaxrs-server</layer>
                  <layer>microprofile-platform</layer>
                  <layer>postgresql-datasource</layer>
               </layers>
               <cloud />
            </configuration>
            <executions>
               <execution>
                  <goals>
                     <goal>package</goal>
                  </goals>
               </execution>
            </executions>
         </plugin>
      </plugins>
   </build>
</project>

Let's analyze the core elements of the Maven plug-in. The first important element is the <feature-pack> tag. The JBoss EAP JAR Maven plug-in uses Galleon's trimming capability to reduce the size and memory footprint of the server. Thus, you can configure the server according to your requirements, including only the Galleon layers that provide the capabilities you need.

To specify the layers we want, I use two feature-packs:

  • wildfly-galleon-plugin specifies the layers provided by JBoss EAP XP.
  • postgresql-layer is the custom layer that builds the data source subsystem described in the previous section. To refer to this layer, I use the Maven GAV coordinates (group ID, artifact ID, and version) specified in the project's pom.xml file.

Those two feature-packs are used by the Galleon framework to build JBoss EAP XP with only the requested subsystems. The <layers> tag specifies the required layers:

  • jaxrs-server: This layer contains the subsystem needed to implement the Servlet, JAX-RS, JPA, and CDI Jakarta EE specifications.
  • microprofile-platform: This decorator layer adds the MicroProfile capabilities to the provisioned server.
  • postgresql-datasource: This is my custom decorator layer that I created to manage the data source subsystem and the JDBC driver.

The <cloud/> tag appears in the <configuration> element of the plug-in configuration in the pom.xml file so that the JBoss EAP Maven JAR plug-in can recognize that you chose the OpenShift platform.

Run Maven locally to create the weather-app-cloud-ready-1.0-bootable.jar JAR file containing all you need to deploy the application into JBoss EAP XP inside OpenShift:

$ mvn clean package

Update the buildConfig

In Part 4 of my previous series, I showed how to create a chained build to produce a runtime image that contained only the resources needed to execute the application. Using the bootable JAR mode with JBoss EAP XP, we can follow the same approach. This time, we import the OpenJDK container images:

# Import image related to OpenJDK 11
$ oc import-image ubi8/openjdk-11:1.10-1 --from=registry.access.redhat.com/ubi8/openjdk-11:1.10-1 --confirm

# Import image related to OpenJDK 11 - Runtime
$ oc import-image ubi8/openjdk-11-runtime:1.10-1 --from=registry.access.redhat.com/ubi8/openjdk-11-runtime:1.10-1 --confirm

I have updated the buildConfig.yaml file under the k8s directory. That file now defines a chained build with two buildConfig objects: weather-app-eap-cloud-ready-build-artifacts and weather-app-eap-cloud-ready. The first object is a base platform image for building and running plain Java 11 applications, such as a fat JAR and a flat classpath. The container image contains S2I integration scripts for deployment on OpenShift.

The second object is a lean, runtime-only container designed to be a base for deploying prebuilt applications. The container does not contain the Java compiler, the JDK tools, or Maven. Here, I put my fat JAR containing the JBoss EAP XP server and the application:

kind: ImageStream
apiVersion: image.openshift.io/v1
metadata:
  name: weather-app-eap-cloud-ready-build-artifacts
  labels:
    application: weather-app-eap-cloud-ready-build-artifacts
---
kind: ImageStream
apiVersion: image.openshift.io/v1
metadata:
  name: weather-app-eap-cloud-ready
  labels:
    application: weather-app-eap-cloud-ready
---
kind: BuildConfig
apiVersion: build.openshift.io/v1
metadata:
  name: weather-app-eap-cloud-ready-build-artifacts
  namespace: redhat-jboss-eap-cloud-ready-demo
  labels:
    build: weather-app-eap-cloud-ready-build-artifacts
spec:
  output:
    to:
      kind: ImageStreamTag
      name: 'weather-app-eap-cloud-ready-build-artifacts:latest'
  resources: {}
  strategy:
    type: Source
    sourceStrategy:
      env:
        - name: ARTIFACT_DIR
          value: weather-app-eap-cloud-ready/target
      from:
        kind: ImageStreamTag
        namespace: redhat-jboss-eap-cloud-ready-demo
        name: 'openjdk-11:1.10-1'
  source:
    type: Binary
    binary: {}
---
kind: BuildConfig
apiVersion: build.openshift.io/v1
metadata:
  labels:
    application: weather-app-eap-cloud-ready
  name: weather-app-eap-cloud-ready
spec:
  output:
    to:
      kind: ImageStreamTag
      name: weather-app-eap-cloud-ready:latest
  source:
    images:
    - from:
        kind: ImageStreamTag
        name: weather-app-eap-cloud-ready-build-artifacts:latest
      paths:
      - sourcePath: /deployments
        destinationDir: ./deployments
    dockerfile: |-
      FROM openjdk-11:1.10-1
      COPY deployments /
      CMD  java -jar /deployments/weather-app-cloud-ready-1.0-bootable.jar
  strategy:
    dockerStrategy:
      imageOptimizationPolicy: SkipLayers
      from:
        kind: ImageStreamTag
        name: openjdk-11-runtime:1.10-1
        namespace: redhat-jboss-eap-cloud-ready-demo
    type: Docker
  triggers:
  - imageChange: {}
    type: ImageChange

Deploy the application

Now it's time to create the ImageStream instances and the chained buildConfig to build and deploy the application:

# Move to the project directory
$ cd weather-app-eap-cloud-ready

# Create the ImageStreams and the chained builds config to make the runtime image with JBoss EAP XP 3 and the application
$ oc create -f k8s/buildConfig.yaml

# Move to the project root
$ cd ..

# Start the build of the application on OpenShift
$ oc start-build weather-app-eap-cloud-ready-build-artifacts --from-dir=. --wait

I suggest checking when the second build is finished by using this command:

$ oc get build weather-app-eap-cloud-ready-1 --watch

After the status moves from Pending to Complete, you can create the weather application for JBoss EAP XP 3 and configure all the needed resources:

$ oc create -f k8s/weather-app-eap-cloud-ready.yaml

You can then test the application, using the steps described in the previous articles, to verify that it works. Although I revisited the way to build and deploy my application, the source code and the features remain the same. I can continue to use all the specifications provided by Jakarta EE and MicroProfile using the fat JAR mode, like the majority of Java cloud-friendly frameworks.

Resource optimization

Using the Galleon framework for a container runtime image reduces the use of expensive resources. In Part 4 of my previous series, I showed how you can save about 35% of the space in the memory image and about 28% of its memory footprint using this approach instead of the traditional container image with the full JBoss EAP XP framework and all the developer tools. I obtained the same result using the bootable JAR approach.

Figure 1 shows the size of the container image with OpenJDK and the bootable JAR (JBoss EAP XP plus the application).

The size of the complete image is only 248.1 MiB.
Figure 1. The size of the complete image is only 248.1 MiB.

Figure 2 shows the memory footprint.

The memory footprint of running image is only 403.1 MiB.
Figure 2. The memory footprint of running image is only 403.1 MiB.

As these figures show, I obtained results similar to the previous series without changing my code.

Conclusion

This article shows how to modernize your application to run in the cloud while implementing the features provided by Jakarta EE and MicroProfile. I demonstrated an approach using a bootable JAR, like the majority of the Java frameworks aimed at cloud deployment, without needing to change any application code. You can start using this approach now or adopt it later.

The impact on your code is very minimal with this approach, so you can keep evolving your applications as you need to. Continuous improvement is key to the success of your architecture.

Last updated: November 8, 2023