HomeDockerMaking Java programs cloud-ready, Part 4: Optimize the runtime environment

Making Java programs cloud-ready, Part 4: Optimize the runtime environment

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 ensuing image is considerably larger than our initial image. This is not optimum because we’ll need to transfer the image over the community 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 assault 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=climate-app-eap-cloud-ready 
$ oc delete is climate-app-eap-cloud-ready
$ oc delete bc climate-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 construct with 2 buildConfig objects: climate-app-eap-cloud-ready-construct-artifacts and climate-app-eap-cloud-ready. The first 1 is the S2I builder image that contains a complete JBoss EAP server with tooling desired during the S2I construct. The second 1 has the runtime image that contains dependencies desired to run JBoss EAP. The first construct creates the JBoss EAP XP instance and the application to be deployed, whereas the second construct excludes the development tools not desired in the manufacturing 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 construct:

kind: ImageStream
apiVersion: image.openshift.io/v1
metadata:
  name: climate-app-eap-cloud-ready-construct-artifacts
  labels:
    application: climate-app-eap-cloud-ready-construct-artifacts
---
kind: ImageStream
apiVersion: image.openshift.io/v1
metadata:
  name: climate-app-eap-cloud-ready
  labels:
    application: climate-app-eap-cloud-ready
---
kind: BuildConfig
apiVersion: construct.openshift.io/v1
metadata:
  name: climate-app-eap-cloud-ready-construct-artifacts
  namespace: redhat-jboss-eap-cloud-ready-demo
  labels:
    construct: climate-app-eap-cloud-ready-construct-artifacts
spec:
  output:
    to:
      kind: ImageStreamTag
      name: 'climate-app-eap-cloud-ready-construct-artifacts:latest'
  sources: {}
  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: construct.openshift.io/v1
metadata:
  labels:
    application: climate-app-eap-cloud-ready
  name: climate-app-eap-cloud-ready
spec:
  output:
    to:
      kind: ImageStreamTag
      name: climate-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: climate-app-eap-cloud-ready-construct-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 construct a bootable JAR application image, which contains a server, a packaged application, and the runtime required to launch the server. As proven in the YAML snippet, there are 2 properties related to the Galleon framework. The first 1 creates a bootable JAR with a full-featured JBoss EAP XP subsystem:

GALLEON_PROVISION_DEFAULT_FAT_SERVER=true

But our goal 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 sources 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 desired 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 construct, named climate-app-eap-cloud-ready-construct-artifacts in the buildConfig.yaml, into the final runtime image as described in the climate-app-eap-cloud-ready construct, 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 construct of the application on OpenShift:

$ oc start-construct climate-app-eap-cloud-ready-construct-artifacts --from-dir=. --wait

I suggest that you check when the second construct finishes with this command:

$ oc get construct climate-app-eap-cloud-ready-1 --watch

After the status moves from Pending to Complete, you can create the climate application for JBoss EAP XP 2 and configure it:

$ oc create -f k8s/climate-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 nonetheless working.

Reviewing the outcomes

Now it’s time to check the return on funding 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 ensuing final application includes features and services that are beneficial for microservice applications operating in the cloud. By repeating the processes proven in the series, you can break your monolithic Java applications into small and unbiased modules without needing to closely change your source code. The ensuing 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.

Go to the source

Most Popular