Featured image for Java.

Red Hat Universal Base Images (UBI) contain the full Red Hat build of OpenJDK. Universal Base Images is available to anyone under the terms of the UBI end user license agreement and is fully supported for Red Hat customers. These "builder" images are designed to be suitable for building and running a wide range of Java-based applications, particularly when used in a Red Hat OpenShift environment. They contain the full Java Development Kit (JDK) including the development tools, Java compiler, Maven, and related build tooling.

The OpenShift source-to-image (S2I) process makes it straightforward to build and update your application's source code within an OpenShift cluster, and your deployments will be updated whenever the underlying image or your application sources are updated. With this workflow, the application is layered on top of the builder image, so the deployment contains the full JDK and Maven tooling. Some developers want their deployments to be based on a slimmer base image; perhaps without Maven, or without the full JDK tooling, or both.

To address this need, Red Hat has released new OpenJDK runtime container images, which do not contain the full JDK or other build tooling. This article explains how to use these new images within an OpenShift environment to automatically deploy your application using the S2I workflow. We will use a Quarkus quickstart as the application source.

S2I and Quarkus quickstart with the builder images

To get started with the Quarkus quickstart, make sure you are logged into an OpenShift instance and run the following commands:

# Build the image on OpenShift
$ oc new-app  --context-dir=getting-started --name=quarkus-quickstart \
 'registry.access.redhat.com/ubi8/openjdk-11~https://github.com/quarkusio/quarkus-quickstarts.git#1.13.3.Final'

# Watch the build, to see when it completes
$ oc logs -f bc/quarkus-quickstart

# Create a route
$ oc expose svc/quarkus-quickstart

# Get the route URI
$ export URI="http://$(oc get route | grep quarkus-quickstart | awk '{print $2}')"

# Test the app!
$ curl $URI/hello/greeting/quarkus

The first command, oc new-app, creates all the OpenShift components needed to build and deploy the quickstart application, namely a BuildConfig, ImageStreams for both the ubi8/openjdk-11 base image and the resulting application image, and a DeploymentConfig and a Service. The command also instantiates a build and a deployment. The oc expose command creates a Route to the deployment, allowing us to test the running web application.

The great thing about this setup is that a new build can be triggered whenever the application sources are updated in their Git repository, or whenever the underlying builder image is updated.

A runtime image

Now for the new bit: We are going to define a new ImageStream for a lean runtime image. As part of this image, we will copy the application artifacts out of the application ImageStream that was created in the previous step. We also need to define the other OpenShift pieces to get the application up and running, as was done automatically for the initial quickstart: A DeploymentConfig, Service, and Route. Figure 1 shows our process.

A diagram of the runtime image process.
Figure 1: The OpenShift process for generating a DIY runtime image.
The OpenShift process for generating a DIY runtime image

First, we need the lean base image. We're going to use the Red Hat UBI8 minimal base image. We need to import that as an ImageStream into OpenShift. That's as easy as the following command:

$ oc create -n openshift -f https://raw.githubusercontent.com/jboss-container-images/openjdk/release/templates/runtime-image-streams.json

Now we define the OpenShift components for our lean application, which we will create one at a time. First, we create the ImageStream into which we'll publish the lean runtime application images:

$ oc create imagestream quarkus-quickstart-runtime

Now we need a BuildConfig to build the image. This is more complicated, so we will define it in a YAML file rather than on the command line. For this and all YAML snippets that follow in this article, save the snippet to a file and load it into OpenShift with:

$ oc create -f snippet.yaml

The BuildConfig follows:

apiVersion: v1
kind: BuildConfig
metadata:
  name: quarkus-quickstart-runtime
spec:
  output:
    to:
      kind: ImageStreamTag
      name: quarkus-quickstart-runtime:latest
  source:
    images:
    - from:
        kind: ImageStreamTag
        name: quarkus-quickstart:latest
      paths:
      - sourcePath: /deployments
        destinationDir: ./deployments
    dockerfile: |-
      FROM -
      COPY deployments /
      CMD  java -jar /deployments/quarkus-run.jar
  strategy:
    type: Docker
    dockerStrategy:
      from:
        kind: ImageStreamTag
        name: ubi8-openjdk-11-runtime:latest
  triggers:
  - type: ConfigChange
  - type: ImageChange
    imageChange:
      automatic: true
      from:
        kind: ImageStreamTag
        name: quarkus-quickstart:latest
  - type: ImageChange
    imageChange:
      automatic: true
      from:
        kind: ImageStreamTag
        name: ubi8-openjdk-11-runtime:latest

Note: The Dockerfile for the image is inline in the YAML. We use a placeholder FROM image of "-"; the dockerStrategy section of the YAML tells OpenShift to override the FROM line with a value that represents the latest image in the ubi8-openjdk-11-runtime image stream.

We copy the /deployments directory from the Quarkus quickstart image into our new lean image. We know that the entirety of the Quarkus quickstart application is present in this directory, so this is all we need to copy. Further steps might be needed depending on the specific application. Finally, we set the lean container’s CMD to invoke java directly, passing the application's JAR on the command line.

Any changes to this configuration, the underlying quarkus-quickstart image, or the base ubi8-openjdk-11-runtime image will trigger a rebuild of this image.

Save the snippet to a temporary file and load it into OpenShift as described earlier. OpenShift should now schedule and start a build of the image. As before, you can watch the logs to see its progress:

$ oc logs -f bc/quarkus-quickstart-runtime

Image sizes

Once the build has completed, we can inspect the result and check its size. First, get the size of the normal quickstart ImageStream, layered on top of the builder image:

$ oc get istag quarkus-quickstart:latest -o json | jq .image.dockerImageMetadata.Size
308976772
=> 295 MiB

For comparison, get the new runtime image:

$ oc get istag quarkus-quickstart-runtime:latest -o json | jq .image.dockerImageMetadata.Size
135747392
=> 129 MiB

The new runtime image is less than half the size of the full UBI8 OpenJDK builder image, which contains the full OpenJDK and Maven.

Finishing touches

Next we need to define the DeploymentConfig, Service, and Route. There's not much to say about these, so I’ve run them together into a single YAML snippet. You can save it and load them in one go via oc create -f, as we did earlier:

apiVersion: v1
kind: DeploymentConfig
metadata:
    name: quarkus-quickstart-runtime
spec:
  replicas: 1
  selector:
    app: quarkus-quickstart-runtime
  template:
    metadata:
      labels:
        app: quarkus-quickstart-runtime
    spec:
      containers:
      - image: ' '
        name: quarkus-quickstart-runtime
        ports:
        - containerPort: 8080
          protocol: TCP
  triggers:
  - type: ConfigChange
  - imageChangeParams:
      automatic: true
      containerNames:
      - quarkus-quickstart-runtime
      from:
        kind: ImageStreamTag
        name: quarkus-quickstart-runtime:latest
    type: ImageChange
---
apiVersion: v1
kind: Service
metadata:
  name: quarkus-quickstart-runtime
spec:
  selector:
    app: quarkus-quickstart-runtime
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 8080
---
apiVersion: v1
kind: Route
metadata:
  name: quarkus-quickstart-runtime
spec:
  to:
    kind: Service
    name: quarkus-quickstart-runtime

Once these are loaded, use the same technique to obtain the route and fetch a "Hello world" URI to make sure all is well:

$ export URI="http://$(oc get route | awk '/quarkus-quickstart-runtime/ {print $2}')"
curl $URI/hello/greeting/quarkus

Wrap-up

The Universal Base Images OpenJDK builder image were designed to meet the needs of a wide variety of customers wishing to build and deploy their software on OpenShift. However, the image's flexibility comes at the cost of image size. Some developers wish to have smaller images or images without the build tooling. Red Hat has introduced the new UBI OpenJDK runtime images to address this need. The method described in this article allows you to customize and build a do-it-yourself (DIY) application image within OpenShift based on the new runtime images. This method also ensures that the image is rebuilt when the constituent parts change.

Future work

The main driver for the new runtime images was to ensure they consisted of as little as possible. The existing builder images contain a number of features that are not present in the runtime images, including (but not limited to) a featureful run script; the Prometheus agent for metric collection; OpenShift readiness probes; default JVM and garbage collection parameters; and a wealth of environment variables for tuning the image. If you want these features, the full builder images might be for you. If, instead, you want the smallest possible runtime images, with the fewest moving parts, and don't mind a bit of DIY, the runtime images might be a better fit.

At Red Hat, we're continually evolving our containers to take advantage of the latest techniques and technologies. We know that image sizes and deployment surface area are important issues for many developers and we are continually working to reduce the size of both the OpenJDK builder and runtime images with each release. We've got some promising developments in the pipeline. Watch this space!

Last updated: April 25, 2022