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:
-
Part 1: An incremental approach using Jakarta EE and MicroProfile
-
Part 3: Integrate MicroProfile services
-
Part 4: Optimize the runtime environment
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.
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
Configure JBoss EAP XP using Galleon
We'll also need to configure JBoss EAP XP to run it using 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
The final image will contain a server, a packaged application, and the runtime required to launch JBoss EAP. As shown in the YAML snippet, there are two properties related to the Galleon framework. The first one creates 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.
Figure 3 shows the new memory footprint.
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.
Last updated: January 12, 2024