Migrate a Spring Boot app to OpenShift

Overview

Imagine you have an existing application you need to move into an OpenShift® Cluster. You also have a requirement to change the runtime without modifying the original source code or using different application servers. This tutorial will show how to migrate and run an existing Spring Boot application, which runs with Apache Tomcat embedded, to an OpenShift Cluster and replace the Tomcat with Open Liberty. The challenge here is that the Spring Boot app should be not modified. The reason for this could be that it should not have a direct relation to the runtime, so that it is easy to shift the workload between different cloud provider like IBM Cloud, OpenShift, or a vanilla Kubernetes cluster.

This tutorial covers the migration of an existing Spring Boot app to an OpenShift Cluster with a different runtime, like an Open Liberty server. The packaging and deployment will be handled with OpenShift Source-to-Image (S2I). An example with Spring Boot and the complete configuration is in GitHub (branch: OpenShift).

We will first analyze the relevant steps to run the Spring Boot app on Open Liberty. The gained knowledge will be used to prepare an OpenShift S2I builder, which will be used to deploy the Spring Boot app in OpenShift Container Platform with Open Liberty.

Prerequisites

Estimated time

Completing this tutorial should take about 60 minutes.

OpenShift introduction

OpenShift is a container runtime based on Kubernetes with additional enterprise-related features, including Source-to-Image (S2I).

Source-to-Image is an OpenShift concept for building and deploying an application using a builder image for building the app. The result is a new Docker image, which will be used for deploying the new app. The benefit is that the builder image can take over the complex and recurrent activities like compiling or preparing the environment (in a Docker container). With S2I, is it also possible to separate any environment- or product-specific activities from the main application source.

Install oc and s2i on the local machine. Consider that, in general, in the web UI of OpenShift under “About” is a link to download the oc artifact. You can also use oc login with a generic token to login into your OpenShift Cluster:

oc login https://c100-e.eu-de.containers.cloud.ibm.com:30019 --token=<your-token-value>

s2i will be used to generate locally the S2I builder image.

Steps

Step 1. Migration to Open Liberty

This step covers the necessary modification to run the Spring Boot app in an Open Liberty server. Open Liberty provides Spring Boot support. This feature will disable the embedded web container in the Spring application and use Liberty.

In Open Liberty, you deploy an app in adding an entry to the server configuration server.xml or using the dynamic approach and place the app in a defined dropins directory. As in our case, the app contains the whole configuration, and when modifying the server.xml every time is not desired, the dropins solution is the preferred one.

For the migration, we will use an official Open Liberty Docker image with a Spring Boot 2 flavor (open-liberty:springBoot2). The special feature is that the included server.xml contains some Spring Boot-related configuration:

<!-- Enable features for Spring Boot 2.0 -->
<featureManager>
  <feature>springBoot-2.0</feature>
  <feature>servlet-4.0</feature>
  <!-- ... -->
</featureManager>

<!-- Configure HTTP endpoint -->
<httpEndpoint id="defaultHttpEndpoint" host="*" httpPort="9080" />

In addition, the springBootUtility is included, which splits the embedded dependencies of the Spring app fat jar from the app code. This optimizes the usage and data transfer during the container image push into the registry. The springBootUtility enables the placement of the app code in a separate layer, above the layer with the dependencies. The springBootUtility is a feature of both Open Liberty and WebSphere® Liberty (learn more in the WebSphere Liberty documentation).

Extract libs from the original Spring Boot app and place the app code in the Liberty dropins directory:

<wlp>/bin/springBootUtility thin \
    --sourceAppPath=hellospringboot.jar \
    --targetLibCachePath=<wlp>/usr/shared/resources/lib.index.cache \
    --targetThinAppPath=<wlp>/usr/servers/helloserver/dropins/spring/hellospringboot.jar

Step 2. Create Docker image with Open Liberty

A modified Dockerfile, which uses Open Liberty and splits a Spring Boot app, looks as follows:

# Open Liberty example with Spring (Boot) application
# use springBootUtility to split the embedded libs from the main logic
ARG IMAGE=open-liberty:springBoot2
FROM ${IMAGE} as staging

USER root
# Symlink servers directory for easier mounts.
RUN ln -s /opt/ol/wlp/usr/servers /servers

ARG JAR_FILE
COPY ${JAR_FILE} /staging/app.jar

# Extract the libs
RUN springBootUtility thin \
 --sourceAppPath=/staging/app.jar \
 --targetThinAppPath=/staging/appThin.jar \
 --targetLibCachePath=/staging/lib.index.cache

# New image using the separated application (cache and main logic)
FROM ${IMAGE}
COPY --from=staging /staging/lib.index.cache /lib.index.cache
COPY --from=staging /staging/appThin.jar /config/dropins/spring/appThin.jar

USER 1001

# Run the server script and start the defaultServer by default.
ENTRYPOINT ["/opt/ol/wlp/bin/server", "run"]
CMD ["defaultServer"]

After the Docker build and run, the app under http://localhost:8099/swagger-ui.html becomes reachable:

$ docker build --rm -t service-playground-liberty -f DockerfileLiberty --build-arg=JAR_FILE=target/service-playground-0.0.1-SNAPSHOT.jar .
$ docker run -d -p 8099:9080 service-playground-liberty:latest

To verify the configuration and environment parameters, use the Actuator Env endpoint at http://localhost:8099/actuator/env.

Step 3. Migration to OpenShift

Now we have a Spring Boot app running in a Docker container with Open Liberty. The next step is to provide the base to create and deploy a Spring Boot app into an OpenShift Cluster. For this, we use the OpenShift Source-to-Image (S2I) mechanism. S2I helps fullfil the requirement that we will not modify the app source, reduce the configuration and the tight coupling to OpenShift.

The base is a new builder image, which process the necessary steps:

  • Compiling the application (from source repository)
  • Extracting the libraries
  • Placing the artifacts in the right directories for Open Liberty

The result is a new Docker image with Open Liberty and the new version of the app.

Step 4. Build S2I builder image

The S2I builder image is a Docker image, which expects s2i files:

  1. assemble — script to compile and assemble the new Docker image, using the source
  2. run — to run the new created Docker image
  3. usage — to hold usage information

With the help of s2i, the initial structure can be created with s2i create s2i-openliberty-springapp s2i-builder and results in the following directory structure and files.

s2i-builder> tree
.
├── Dockerfile
├── Makefile
├── README.md
├── s2i
│   └── bin
│       ├── assemble
│       ├── run
│       ├── save-artifacts
│       └── usage
└── test
    ├── run
    └── test-app
        └── index.html

4 directories, 9 files

In the following steps, the files are adapted to our needs.

The Dockerfile for the Spring Boot builder image looks as follows:

# OpenShift S2I Builder Image for
# Open Liberty example with Spring (Boot) application
# use springBootUtility to split the embedded libs from the main logic
ARG IMAGE=open-liberty:springBoot2
FROM ${IMAGE} as staging

LABEL io.k8s.description="Used for building and running Spring Boot application on Open Liberty" \
      io.k8s.display-name="SpringApp-In-OpenLiberty" \
      io.openshift.expose-services="9080:http" \
      io.openshift.tags="builder,liberty,open-liberty" \
      io.openshift.s2i.destination="/tmp" \
      io.openshift.s2i.scripts-url="image:///opt/s2i/"

USER root
RUN   apt-get update \
      && apt-get -y install maven \
      && apt-get -y install openjdk-8-jdk \
      && apt-get clean

# Copy Scripts to build s2i
COPY ./s2i/bin/assemble /opt/s2i/
COPY ./s2i/bin/run /opt/s2i/
COPY ./s2i/bin/usage /opt/s2i/

# Create some dir and change permissions
RUN mkdir -p /home/default/.m2/repository \
    && mkdir -p /config/dropins/spring/ \
    && chown -R 1001:0 /opt/s2i/ && chmod -R +x /opt/s2i/ \
    && chown -R 1001:0 /home/default/.m2 && chmod g=u /home/default/.m2 \
    && chown -R 1001:0 /config/dropins/spring && chmod g=u /config/dropins/spring

# change JAVA_HOME to have JDK during maven build
ENV JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64

USER 1001

Step 5. Create the assemble script

The assemble script builds and packages the source code. The created Spring Boot app will be extracted with springBootUtility and the files moved to the Open Liberty paths.

S2I assemble script:

# compile and package application
cd /tmp/src
mvn clean package

# split libraries from the main logic
mkdir -p /tmp/artifacts
cp /tmp/src/target/*.jar /tmp/artifacts/app.jar

springBootUtility thin \
 --sourceAppPath=/tmp/artifacts/app.jar \
 --targetThinAppPath=/tmp/artifacts/appThin.jar \
 --targetLibCachePath=/tmp/artifacts/lib.index.cache

# place the files for Open Liberty in the right dirs
cp -r /tmp/artifacts/lib.index.cache/* /lib.index.cache
cp -r /tmp/artifacts/appThin.jar /config/dropins/spring/appThin.jar

Step 6. Create the run script

The S2I run script is very simple and contains only the command to run Open Liberty server:

# run the application
/opt/ol/wlp/bin/server run defaultServer

Step 6. Build the S2I builder image

The following commands build the builder image s2i-openliberty-springapp. We then test the builder image with the Spring Boot app’s local repository and build a new app Docker image test-s2i-springapp.

s2i-builder> docker build -t s2i-openliberty-springapp -f DockerfileS2IBuilderImage .

spring-example-repo> s2i build . s2i-openliberty-springapp test-s2i-springapp

spring-example-repo> docker run -d -p 8099:9080 -u 7887 test-s2i-springapp

Now the app under the same URL is reachable: http://localhost:8099/. The big difference here is only the build and deployment process. The app source does not contain any knowledge that the runtime is Open Liberty or OpenShift Cluster. This is all handled during the deployment process, which includes also the (re-)build and packing of the app.

To deploy the app in an OpenShift Cluster, we need to publish the new S2I builder image in OpenShift and create a new app.

Step 7. Publish S2I builder image

To deploy the app in OpenShift, we have to register the new S2I builder image in OpenShift, so we will push the Docker image to the internal OpenShift Container Registry in a general project named common. OpenShift automatically creates an ImageStream, which is now available and can be used any time to create a new app, if that kind of S2I builder is needed:

# Login and create common project
$ oc login https://c100-e.eu-de.containers.cloud.ibm.com:30488 --token=dct7....

$ oc new-project common

# Create tag and push to OpenShift Registry
$ docker tag s2i-openliberty-springapp:v1.0.0 docker-registry-default.article-cluser-123456789abcdefgh-0001.eu-de.containers.appdomain.cloud/common/s2i-openliberty-springapp:v1.0.0

$ docker login -u token -p $(oc whoami -t) docker-registry-default.article-cluser-123456789abcdefgh-0001.eu-de.containers.appdomain.cloud

$ docker push docker-registry-default.article-cluser-123456789abcdefgh-0001.eu-de.containers.appdomain.cloud/common/s2i-openliberty-springapp:v1.0.0
The push refers to repository [docker-registry-default.article-cluser-123456789abcdefgh-0001.eu-de.containers.appdomain.cloud/common/s2i-openliberty-springapp]
5d0d868c79e0: Pushed
965d6a798d5e: Pushed
7d11cd4a4782: Pushed
f9725c11f15d: Pushed
350befc62911: Pushed
db75ce8a74ec: Pushed
126d273c10f4: Pushed
b75afb1bc4ce: Pushed
69f2bba0832b: Pushed
895c7b09f601: Pushed
bd4b24484dda: Pushed
4ce08090970e: Pushed
e79142719515: Pushed
aeda103e78c9: Pushed
2558e637fbff: Pushed
f749b9b0fb21: Pushed
v1.0.0: digest: sha256:0773401a2c231dc0b7cd811fba244adedcba4315437778ba828e9bc2ff516304 size: 3660

$ oc get is -n common
NAME                        IMAGE REPOSITORY                                                                    TAGS     UPDATED
s2i-openliberty-springapp   image-registry.openshift-image-registry.svc:5000/common/s2i-openliberty-springapp   v1.0.0   2 minutes ago

After the Docker push into the OCP Container Registry, we have an ImageStream named common/s2i-openliberty-springapp. This ImageStream can be used for any Spring Boot app to run with an Open Liberty server.

Step 8. Install application

This section covers the installation of the application using the new S2I builder image, which is in a common namespace. The access should be granted first from the new project test-s2i:

$ oc adm policy add-role-to-user -n common system:image-puller system:serviceaccount:test-s2i:builder

Now we can create the new project test-s2i and deploy the new app using a Git repository containing source code and refer the S2I builder image (stream) common/s2i-openliberty-springapp:

$ oc new-project test-s2i

$ oc new-app --name test-s2i-springapp -i common/s2i-openliberty-springapp:v1.0.0 https://github.com/IBM/spring-microservice

--> Found image 2706284 (2 months old) in image stream "common/s2i-openliberty-springapp" under tag "v1.0.0" for "common/s2i-openliberty-springapp:v1.0.0"

    SpringApp-In-OpenLiberty
    ------------------------
    Used for building and running Spring Boot application on Open Liberty

    Tags: builder, liberty, open-liberty

    * The source repository appears to match: jee
    * A source build using source code from https://github.com/IBM/spring-microservice will be created
      * The resulting image will be pushed to image stream tag "test-s2i-springapp:latest"
      * Use 'oc start-build' to trigger a new build
    * This image will be deployed in deployment config "test-s2i-springapp"
    * Ports 9080/tcp, 9443/tcp will be load balanced by service "test-s2i-springapp"
      * Other containers can access this service through the hostname "test-s2i-springapp"

--> Creating resources ...
    imagestream.image.openshift.io "test-s2i-springapp" created
    buildconfig.build.openshift.io "test-s2i-springapp" created
    deploymentconfig.apps.openshift.io "test-s2i-springapp" created
    service "test-s2i-springapp" created
--> Success
    Build scheduled, use 'oc logs -f bc/test-s2i-springapp' to track its progress.
    Application is not exposed. You can expose services to the outside world by executing one or more of the commands below:
     'oc expose svc/test-s2i-springapp'
    Run 'oc status' to view your app.

We see that the image stream common/s2i-openliberty-springapp is used and the initial build is triggered.

The log from builder config gives some insight about the build process:

$ oc logs -f bc/test-s2i-springapp

...
[INFO] Replacing main artifact with repackaged archive
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 37.270 s
[INFO] Finished at: 2019-10-27T18:20:42+00:00
[INFO] Final Memory: 49M/2298M
[INFO] ------------------------------------------------------------------------
Creating a thin application from: /tmp/artifacts/app.jar
Library cache: /tmp/artifacts/lib.index.cache
Thin application: /tmp/artifacts/appThin.jar

Pushing image docker-registry.default.svc:5000/test-s2i/test-s2i-springapp:latest ...
Pushed 0/17 layers, 0% complete
Pushed 1/17 layers, 6% complete
Pushed 2/17 layers, 12% complete
Pushed 3/17 layers, 18% complete
...
Pushed 16/17 layers, 94% complete
Pushed 17/17 layers, 100% complete
Push successful

From the logs, we see:

  • That the builder image is used for building
  • The source project is successfully compiled and built
  • SpringBootUtility is used to create the thin app
  • The build and push of the newly created Docker image

After a while, we have a running pod:

$ oc get pods
NAME                          READY   STATUS      RESTARTS   AGE
test-s2i-springapp-1-build    0/1     Completed   0          4m2s
test-s2i-springapp-1-hhwv6    1/1     Running     0          81s

$ oc get svc
NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)             AGE
test-s2i-springapp   ClusterIP   172.30.178.69   <none>        9080/TCP,9443/TCP   4m6s

To reach the application from outside, we expose the service:

$ oc expose svc test-s2i-springapp
route.route.openshift.io/test-s2i-springapp exposed

$ oc get routes
NAME                 HOST/PORT                                                              PATH   SERVICES             PORT       TERMINATION   WILDCARD
test-s2i-springapp   test-s2i-springapp-test-s2i.article-cluser-123456789abcdefgh-0001.eu-de.containers.appdomain.cloud          test-s2i-springapp   9080-tcp                 None

The Spring Boot main page is now reachable:

$ curl test-s2i-springapp-test-s2i.article-cluser-123456789abcdefgh-0001.eu-de.containers.appdomain.cloud

Summary

This tutorial showed how to migrate an existing Spring Boot application into the cloud. Using the Source-to-Image feature from OpenShift allows for a smooth migration without any changes to the app source code. In addition, the app remains without a closer coupling to the (cloud) runtime environment. Overall, it’s a nice technical approach to moving existing workload into the right cloud environment.

Hafid Haddouti