Featured image for Java topics.

This is the final article in a series where we are updating a monolithic Java EE application to function as a microservice and run in a distributed cloud environment such as Red Hat OpenShift. In the first article, we set up the legacy Java application and defined our goals. Then, we upgraded the Java environment to Jakarta EE. In the last article, we used MicroProfile to prepare the application for use in a distributed environment.

Read the whole series:

We now have all the functionality we planned to add to our cloud-ready Java application. However, the resulting image is substantially larger than our initial image. This is not optimal because we'll need to transfer the image over the network and run it on a platform-as-a-service (PaaS) in the cloud. Resources such as memory, CPU, and RAM use factor into the costs charged by a PaaS provider. In this final article, we'll optimize the runtime to reduce the image's size and memory footprint. The benefits of optimizing the runtime include:

  • Better cloud-resource utilization.
  • Decreasing startup and scale-up time.
  • Minimizing the attack surface.

Runtime optimization with JBoss EAP, JBoss EAP XP, and Galleon

We'll use Red Hat JBoss Enterprise Application Platform (JBoss EAP) and JBoss EAP XP to decrease the size of our application image while also increasing container security. First, we'll develop a runtime image that eliminates development tools (such as Maven artifacts) that were present in the original Source-to-Image (S2I) environment. Then, we'll use Galleon to trim the application features and provide customization for JBoss EAP and its image’s footprint.

Note that we'll use the same GitHub repository we've used for the previous articles in the series. To start, switch to the git tag that contains the source code used to implement the Galleon version:

$ git checkout tags/Galleon_Runtime_version

Now, delete the previous version of the application to start with a clean environment:

$ oc delete all --selector app=weather-app-eap-cloud-ready 
$ oc delete is weather-app-eap-cloud-ready
$ oc delete bc weather-app-eap-cloud-ready

Import the image for the JBoss EAP XP 2.0 OpenJDK 11 runtime:

$ oc import-image jboss-eap-7/eap-xp2-openjdk11-runtime-openshift-rhel8 --from=registry.redhat.io/jboss-eap-7/eap-xp2-openjdk11-runtime-openshift-rhel8 --confirm

Update the buildConfig

Now let's focus on the buildConfig.yaml file under the k8s directory. In that file, I defined a chained build with two buildConfig objects: weather-app-eap-cloud-ready-build-artifacts and weather-app-eap-cloud-ready. The first one is the S2I builder image that contains a complete JBoss EAP server with tooling needed during the S2I build. The second one has the runtime image that contains dependencies needed to run JBoss EAP. The first build creates the JBoss EAP XP instance and the application to be deployed, whereas the second build excludes the development tools not needed in the production environment. Figure 1 summarizes the components of the development process and their relationships.

Diagram with the steps needed to obtain a runtime image.
Figure 1: Components of builds for our containerized application.

Here is a snapshot of the chained build:

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:
      from:
        kind: ImageStreamTag
        namespace: redhat-jboss-eap-cloud-ready-demo
        name: 'eap-xp2-openjdk11-openshift-rhel8:latest'
  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:
    dockerfile: |-
      FROM eap-xp2-openjdk11-runtime-openshift-rhel8
      COPY /server $JBOSS_HOME
      USER root
      RUN chown -R jboss:root $JBOSS_HOME && chmod -R ug+rwX $JBOSS_HOME
      USER jboss
      CMD $JBOSS_HOME/bin/openshift-launch.sh
    images:
    - from:
        kind: ImageStreamTag
        name: weather-app-eap-cloud-ready-build-artifacts:latest
      paths:
      - sourcePath: "/s2i-output/server/"
        destinationDir: "."
  strategy:
    dockerStrategy:
      imageOptimizationPolicy: SkipLayers
      from:
        kind: ImageStreamTag
        name: eap-xp2-openjdk11-runtime-openshift-rhel8:latest
        namespace: redhat-jboss-eap-cloud-ready-demo
    type: Docker
  triggers:
  - imageChange: {}
    type: ImageChange

Switch JBoss EAP XP to bootable JAR mode

We'll also need to configure JBoss EAP XP to run in a bootable JAR mode so that you can enable the runtime image. To configure this mode, I set the following environment variables in the environment file under the .s2i directory:

#GALLEON_PROVISION_DEFAULT_FAT_SERVER=true
GALLEON_PROVISION_LAYERS=jaxrs-server,microprofile-platform
S2I_COPY_SERVER=true

We can use the bootable JAR to build a bootable JAR application image, which contains a server, a packaged application, and the runtime required to launch the server. As shown in the YAML snippet, there are two properties related to the Galleon framework. The first one creates a bootable JAR with a full-featured JBoss EAP XP subsystem:

GALLEON_PROVISION_DEFAULT_FAT_SERVER=true

But our target is not only to have a slim and more secure container image that omits unnecessary tools. We also want to improve the use of cloud resources by removing unused subsystems from JBoss EAP XP. For this reason, I commented out the GALLEON_PROVISION_DEFAULT_FAT_SERVER property. To include only the necessary subsystems, I also set the GALLEON_PROVISION_LAYERS property with the names of the subsystems needed to run my application. The jaxrs-server subsystem provides support for JAX-RS and JPA, while the microprofile-platform subsystem includes the MicroProfile capabilities we added in Part 3.

I also set the property S2I_COPY_SERVER to copy the result of the first build, named weather-app-eap-cloud-ready-build-artifacts in the buildConfig.yaml, into the final runtime image as described in the weather-app-eap-cloud-ready build, which is always set in the buildConfig.yaml file. Without this property, you can't complete this step.

Create the new runtime image

Now it’s time to create the ImageStreams and the chained buildConfig to make the runtime image with JBoss EAP XP 2 and the application:

$ oc create -f k8s/buildConfig.yaml

Then, start the build of the application on OpenShift:

$ oc start-build weather-app-eap-cloud-ready-build-artifacts --from-dir=. --wait

I suggest that you check when the second build finishes with 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 2 and configure it:

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

You can then test your application, using the steps I have described in the previous articles, to verify that it is still working.

Reviewing the outcomes

Now it’s time to check the return on investment for the operations we've just performed. Figure 2 shows the new container image size.

The size of the final, optmized image is only 294.6 MB.
Figure 2: Summary and size information after our upgrade to remove unneeded components.

Figure 3 shows the new memory footprint.

The final, optmized image uses only 718.9 MB of memory.
Figure 3: Memory use after our upgrade to remove unneeded components.

Consider these outcomes:

  • Container image size: The previous application image, with Jakarta EE and MicroProfile features plus all of JBoss EAP XP and RHEL 8 UBI, takes up 455 MB. The final image, obtained through the optimizations we carried out in this article, is 294 MB, a savings of 35%.
  • Memory footprint: The previous application release, with Jakarta EE and MicroProfile features, plus all of JBoss EAP XP and RHEL 8 UBI, requires 1,000 MB of memory. The final release, obtained through optimization, requires 718 MB of memory, a savings of 28%.

Conclusion to Part 4

This series has gone through the steps to modernize a legacy Java EE application using Jakarta EE and Eclipse MicroProfile. The resulting final application includes features and services that are beneficial for microservice applications running in the cloud. By repeating the processes shown in the series, you can break your monolithic Java applications into small and independent modules without needing to heavily change your source code. The resulting runtime environment is:

  • Optimized for the cloud and containers
  • Lightweight, with a flexible architecture
  • More productive for developers
  • Flexible in management, configuration, and administration
  • Oriented to supporting and standardizing microservices development
  • Based entirely on open source tools and standards

Don’t stop evolving all of your applications! Continuous improvement is the key to the success of your architecture.

More about modernization: Application modernization patterns with Apache Kafka, Debezium, and Kubernetes.

Comments