Examining simple build and deployment operations on OpenShift 3/4

OpenShift logo
Note:
I wrote this article originally for OpenShift 3. I'm please to say that, although the architecture of OpenShift 4 is quite different, the command-line interface remains very similar, and the procedures I describe work on OpenShift 4 as well
OpenShift 3 is designed to make simple application deployment more-or-less automatic -- you can go directly from source code in a repository, to an application running in a container (pod) with no detailed understanding of the internal operations involved. However, such an understanding is necessary to set up more complicated build operations.

This article describes how to deploy a simple Java application, which is provided as an executable JAR file, by creating a build configuration and a deployment configuration. All OpenShift steps are carried out on the command line using the oc tool, and we will inspect the OpenShift artefacts created by the infrastructure at each step.

To follow the steps in this example, you will need a Java 1.8 JDK, and an installation of Maven (and probably some experience of setting it up and using it). I don't assume much knowledge of OpenShift 3, but you need to know what a pod is, and have some understanding of containers.

Create a simple Apache Camel application from an archetype

For this demonstration I will be using an application based on Fuse Integration Services (FIS) -- this is a framework for building Apache Camel applications based on Spring Boot. There is no particularly compelling reason for this choice, other than that I need to deploy FIS applications on OpenShift for other purposes. This demonstration would work with any self-contained Java application that can be run from a JAR file.

Get the source

A sample FIS Camel application can be built from a Maven archetype, like this:
$ mvn org.apache.maven.plugins:maven-archetype-plugin:2.4:generate \
  -DarchetypeCatalog=https://maven.repository.redhat.com/ga/io/fabric8/archetypes/\
archetypes-catalog/2.2.195.redhat-000004/archetypes-catalog-2.2.195.redhat-000004-archetype-catalog.xml \
  -DarchetypeGroupId=org.jboss.fuse.fis.archetypes \
  -DarchetypeArtifactId=spring-boot-camel-xml-archetype \
  -DarchetypeVersion=2.2.195.redhat-000004
Fill in the prompted values -- the exact input is unimportant, but I chose fis-test for the artefact name, which also becomes the name of the directory that Maven creates for the source.

The application includes a lot of infrastructure, including the whole of the Apache Camel core, but all it does in its stock form is write messages to standard out.

Build and test the JAR

cd to the newly-created directory, and then
$ mvn package
This creates a JAR file in the target directory. It should be possible to run the application locally, to see the output it produces. Of course, in a real development exercise you would probably want to run unit tests, etc., as well.
$ java -jar target/fis-test-1.0-SNAPSHOT.jar

Create an OpenShift project

Log into OpenShift at the command line (oc login...) and create a new OpenShift project; the project name and description are unimportant for our present purposes.
$ oc new-project test-project

Create an OpenShift build configuration

In OpenShift, a build configuration (BC) is a specification (in the form of a Kubernetes object) that describes how to build a deployable image (usually a Docker or Podman image) from some kind of source. For our purposes it is important to note that "source" is interpreted very broadly. Although we are using the "source-to-image" process here, the source is an executable Java JAR file. In fact, so far as OpenShift is concerned, "source" is anything that isn't an image -- it might even be a configuration file. If we have an image, we can skip the build step completely, and go directly to creating a deployment configuration (see below).

Set up a new OpenShift build configuration, using the stock Java builder image, and indicating that the build will take a binary as input (a JAR, in this case).
$ oc new-build --binary=true --name=fis-test --image-stream=redhat-openjdk18-openshift:1.2
The name fis-test will be used for the created image, and for a number of other purposes. To see the details of the build configuration:
$ oc describe bc/fis-test
This shows the following information, among other things. Note that the build will produce an image called fis-test, and that there is no source for the image yet: we will provide it when invoking the build from the command line.
Strategy:	Source
From Image:	ImageStreamTag openshift/redhat-openjdk18-openshift:1.2
Output to:	ImageStreamTag fis-test:latest
Binary:		provided on build
You can also view, and perhaps edit, the Kubernetes object definition in YAML format by running
$ oc edit bc/fis-test

Build the application image

Start the build defined by the fis-test build configuration, uploading the JAR built earlier by Maven:
$ oc start-build fis-test --from-file=target/fis-test-1.0-SNAPSHOT.jar --follow
The --follow switch here indicates that the command should not complete until the build does.

OpenShift creates a new pod to do the build, whose name is based on the build configuration name -- in this case, fis-test-1-build. The pod takes as its image the builder image specified in the new-build command.

To start the build, the file (or directory) specified on the command line is packed into a tar file, and then becomes the builder image's standard input. OpenShift runs the tar command in the builder's container, unpacking the tar file from stdin into a directory -- /tmp/src by default. It then runs the script /usr/local/bin/s2i/assemble which, in this particular example, simply copies the JAR files from /tmp/src to /deployments. The Java builder image can manage other kinds of Java souce or binary, and actions will be different in those cases: in some cases, actual Java compilation may be performed, although this is not necessary in the present example.

After running assemble (and some other scripts that aren't relevant in this case) the whole contents of the filesytem of the builder pod's filesystem are pushed to the docker registry as a new image. Consequently, we end up with the a new image called fis-test which is almost exactly the same as redhat-openjdk18-openshift, but with our JAR file in /deployments. It is the presence of this JAR that will trigger a JVM execution when the new image is deployed and executed.

The fact that the new image contains everything that was in the builder image is a point of contention for some OpenShift users. A case can certainly be made that a production container ought not to contain build-related tooling, both for reasons of resource management and for security. If this is a problem, then a different build mechanism will need to be used, or a second "chained" build step will be needed to sanitize the generated image.

We can list the builds in the current project like this, and we should see one completed build:
$ oc get builds
NAME         TYPE      FROM      STATUS     STARTED         DURATION
fis-test-1   Source    Binary    Complete   7 minutes ago   1m5s
We should also see one completed pod, that was used to do the build:
$ oc get pods
NAME               READY     STATUS      RESTARTS   AGE
fis-test-1-build   0/1       Completed   0          8m
This pod could now be deleted -- it won't be used for anything else; presumably it is retained so its logs can be inspected if the build fails.

Deploy the image into an OpenShift pod

We now have one new image stream in the project's namespace:
$ oc get is
NAME       DOCKER REPO                             TAGS      UPDATED
fis-test   172.30.1.1:5000/test-project/fis-test   latest    8 minutes ago
The image stream consists of one image -- the one with the default tag "latest". We need to tell OpenShift how to deploy and run this image in one or more pods; this requires the creation of a deployment configuration.
$ oc new-app fis-test
Note that the name 'fis-test' is the name of the image stream -- there are many other possible inputs to new-app, including source code repositories. new-app can create build configurations as well but, so far as I know, we need to use oc new-build explicitly to provide an executable to OpenShift.

Looking at the list of deployment configurations:
$ oc get dc
NAME       REVISION   DESIRED   CURRENT   TRIGGERED BY
fis-test   1          1         1         config,image(fis-test:latest)
we see that the deployment is (by default) triggered by a configuration change, or a change to the image ``fis-test:latest``. In this case, providing the configuration counts as a trigger, and the deployment and provisioning steps will be triggered without further intervention. So you should already see one pod running:
$ oc describe dc fis-test|grep Replicas
	Replicas:	1 current / 1 desired
Since the build configuration was created using default values, the desired replica count (number of pods) will be 1.

The single, new pod is created by a transient deployer pod. The name of this pod, in this example, will be fis-test-1-deploy. Unlike the builder pod, however, this pod is not retained after completion. You may see it briefly flash into life if you watch the pod list whilst doing a deployment.

Since the image for the new pod was created by the source-to-image process using the script /usr/local/s2i/assemble, the entry point for execution will be /usr/local/s2i/run. The run script delegates (in this simple case) to /opt/run-java/run-java.sh, which detects the JAR file in /deployments, and executes it using java -jar.... The command line specifies the use of the Jolokia Java agent, so that the user can have a graphical view of the JVM operation.

Further builds

After modifying the application, subsequent OpenShift builds are carried out by repeating the oc start-build command, which will trigger a rebuild and redeployment in OpenShift. The infrastructure will create new build pods and deployer pod as necessary, numbering them sequentially.

Note that the default redeployment strategy is 'Rolling', which will cause the a new pod to be started with the new code, before the old one is shut down. Depending on the application, this may be inappropriate, and it might be advisable to change the deployment strategy to 'Recreate' (e.g., using oc edit dc/fis-test).

Closing remarks

In practice, I would expect that substantial OpenShift projects would be built using some form of pipeline process, perhaps coordinated by a build manager like Jenkins. The build manager would create new OpenShift pods to carry out specific build and deployment operations. Although OpenShift makes it possible to go directly from a source code repository to a running container, the large number of default settings involved in this operation make it less suitable for large-scale development work.

I've used the command-line for everything in this example, not just because I think it makes the detailed steps clearer, but because I think that the terminology it uses is more apposite. The OpenShift web console, although powerful in its own right, uses terms like 'build' and 'deployment' in non-specific ways. The web console uses the term 'deployment' to mean both a deployment configuration, and the process of executing a deployment. If you already understand the OpenShift build and deployment philosophy that shouldn't be a problem, but the oc command doesn't mix up the different concepts.