A Clojure S2I Builder for OpenShift

Following on to my previous quickstart on getting up and running with Clojure on OpenShift, I wanted to share my notes on taking it to the next level with a custom Clojure Source-to-Image (S2I) builder image.

As a quick background, OpenShift supports three build strategies:

  1. OpenShift builds all (binaries and images) from source.
  2. OpenShift builds some – you build binaries outside OpenShift, and OpenShift builds and deploys images from those binaries.
  3. OpenShift builds none – you create Docker images completely outside OpenShift and then OpenShift deploys them.

My previous quickstart used strategy 2 where you set up a Clojure environment outside OpenShift (for example, on your Mac) to create a binary (a fat JAR) and then use OpenShift’s officially supported Java S2I builder image to build and deploy an application image using the fat JAR.

In this quickstart we use strategy 1: We set up an environment where the only thing you do outside OpenShift is edit Clojure code, and then you do everything else inside OpenShift. A key feature of this setup is that we use OpenShift’s incremental build capability so that Leiningen/Maven doesn’t re-download half the Internet (that is, all your dependency JARs) every time you change your code and rebuild.

Part 1: Using the Clojure S2I Builder Image

In Part 2, I’ll explain how I created the builder image in case anyone wants to fork and tweak it and/or use it as an example for creating builders for other languages. I’ve put the Clojure builder image I created up on DockerHub (with source on GitHub) so you can start right away here in Part 1 and see what the use case is and perhaps never even need to get into the details of the builder image.

The following assumes you have:

  1. An account on OpenShift Online (v3) or Minishift running locally.
  2. The OpenShift command line interface (CLI).

To get started, login to your OpenShift or Minishift instance (see previous quickstart or Minishift instructions:

    oc login <your OpenShift/Minishift>

and then create an application build image using my Clojure S2I builder image (from Docker Hub) and my cpjhello test app from GitHub:

    oc new-project testclj
    oc import-image mpiech/s2i-clojure --confirm
    oc new-build s2i-clojure~https://github.com/mpiech/cpjhello --name=cpjhelloworld

Alternatively, you could also use:

oc new-build mpiech/s2i-clojure~https://github.com/mpiech/cpjhello
--name=cpjhelloworld

Now we have to tweak our image so that it does incremental builds. If you don’t do this, every time you rebuild your app, Leiningen/Maven will redownload all the dependency JARs. There may be a command-line way to set the incremental flag, but I haven’t figured it out; the way I got it working was to edit the yaml using the OpenShift console as follows:

  1. In the left nav of the OpenShift console, click Build->Builds
  2. Click the link to the image you just created, cpjhelloworld
  3. Click (in the upper right) Actions->Edit yaml

    scroll down in the edit window to the strategy block and add incremental: true at the same indent level as from: so that that snippet looks like this:

      strategy:
        type: Source
        sourceStrategy:
          from:
            kind: ImageStreamTag
            namespace: testclj
            name: 's2i-clojure:latest'
          incremental: true
    
  4. Click Save

Now if you do a rebuild with:

    oc start-build cpjhelloworld --follow

you should see output along the lines of:

    build "cpjhelloworld-2" started
    Cloning "https://github.com/mpiech/cpjhello" ...
    Commit: 04cc579c108ff77c61775e0cb0633227f625722a (Added nREPL setup)
    Author: MICHAEL PIECH <mpiech@TaklaMaka.local>
    Date:   Tue Mar 28 19:23:09 2017 -0700
    Pulling image "mpiech/s2i-clojure@sha256:a9b7777b4233ad68dccebe9d9e83ef6cdd24beca0abc0bcf1cd3e545327e3ab3" ...
    Pulling image "mpiech/s2i-clojure@sha256:a9b7777b4233ad68dccebe9d9e83ef6cdd24beca0abc0bcf1cd3e545327e3ab3" ...
    Pulling image "172.30.98.11:5000/testclj/cpjhelloworld:latest" ...
    ---> Restoring build artifacts...
    ---> Installing application source...
    ---> Building application from source...
    Compiling cpjhello.handler.main

    Pushing image 172.30.98.11:5000/testclj/cpjhelloworld:latest ...
    Pushed 4/11 layers, 36% complete
    Pushed 5/11 layers, 45% complete
    Pushed 6/11 layers, 55% complete
    Pushed 7/11 layers, 64% complete
    Pushed 8/11 layers, 88% complete
    Pushed 9/11 layers, 99% complete
    Pushed 10/11 layers, 100% complete
    Pushed 11/11 layers, 100% complete
    Push successful

The key line is Restoring build artifacts..., which indicates that the incremental build mechanism successfully copied already-downloaded dependency JARs from the old image to the new.

Now create and deploy an app with your new image:

    oc new-app cpjhelloworld

Now you should be able to go back into the OpenShift console, click Overview, click cpjhello, click Add route, and then click the route to see Hello World.

And that’s it! Now you have a basic Clojure build chain for OpenShift. Read on to learn the guts of the builder.

Note: You may want to use artifact caching in development, but not in production, as it could increase the size of the images, slow the build, and cause other unexpected output results.

Part 2: Creating the Clojure S2I Builder Image

For this part you will need the following installed in your environment (I’ve only tested this on a Mac):

  1. Docker
  2. S2I

S2I is a standalone command-line tool; a helpful backgrounder is this blog post by Maciej Szulik.

Run S2I in a working directory:

    s2i create s2i-clojure s2i-clojure

and S2I creates a directory structure s2i-clojure. Maciej’s post explains the structure and what the files do, so I’ll just show the specific edits I make.

First we edit the Dockerfile. This builder image is based on Centos, in addition to which we need two things, the JDK and the Leiningen Clojure build tool, which wraps Maven. Note that there are some choices here as to where to put things and as what user. For example, you could put lein in /usr/local/bin as root, but to keep things simple I just put it in the home directory of the image. The way lein works is that once you install the script you have to run it once so that it bootstraps all its supporting files.

    # s2i-clojure
    FROM openshift/base-centos7
    MAINTAINER Mike Piech <mpiech@redhat.com>

    ENV BUILDER_VERSION 1.0

    LABEL io.k8s.description="Platform for building Clojure apps" \
    io.k8s.display-name="Clojure s2i 1.0" \
    io.openshift.expose-services="8080:http" \
    io.openshift.tags="builder,clojure"

    RUN yum -y install java-1.8.0-openjdk-devel && yum clean all

    RUN curl https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein -o ${HOME}/lein
    RUN chmod 775 ${HOME}/lein
    RUN ${HOME}/lein

    # TODO (optional): Copy the builder files into /opt/app-root
    # COPY ./<builder_folder>/ /opt/app-root/

    COPY ./s2i/bin/ /usr/libexec/s2i

    RUN chown -R 1001:1001 /opt/app-root

    # This default user is created in the openshift/base-centos7 image
    USER 1001

    EXPOSE 8080

    CMD ["/usr/libexec/s2i/usage"]

Next edit the assemble script. We’re doing three things here:

  1. First is the key step to support the incremental builds, namely the copying of the Maven repository (.m2) into the location expected by S2I. Note that the default test created by S2I, [ "$(ls /tmp/artifacts/ 2>/dev/null)" ], doesn’t pick up .m2 because the directory begins with a ., which is why I changed it to [-d /tmp/artifacts/.m2] (it took me a while to discover this subtelty).
  2. The second step is to move the source code into the home directory for convenience (we probably could just leave it in /tmp, but I haven’t thought through the implications of that; the s2i-generated assemble script as well as all the examples I saw do the cp or mv).
  3. The third step is to use lein to build the Clojure source into a fat JAR (“app-standalone.jar”). Again there’s a choice as to where we put the JAR; I just put it in the home directory for simplicity.
    if [[ "$1" == "-h" ]]; then
    exec /usr/libexec/s2i/usage
    fi

    # Restore artifacts from the previous build (if they exist).
    #
    if [ -d /tmp/artifacts/.m2 ]; then
    echo "---> Restoring build artifacts..."
    mv /tmp/artifacts/.m2 ${HOME}/
    fi

    echo "---> Installing application source..."
    mv /tmp/src ${HOME}/src

    echo "---> Building application from source..."

    pushd ${HOME}/src > /dev/null
    mv "$(${HOME}/lein ring uberjar | sed -n 's/^Created \(.*standalone\.jar\)/\1/p')" ${HOME}/app-standalone.jar
    popd > /dev/null

Now edit run. We just java -jar our fat JAR:

    exec java -jar ${HOME}/app-standalone.jar

Now that we’ve edited all the necessary S2I scripts, cd to the s2i-clojure directory where the Makefile is and do:

    make

You should see a successful make. You can check against https://github.com/mpiech/s2i-clojure if anything seems amiss.

You now have an S2I builder image called s2i-clojure in your local Docker repository that you can use as you did in Part 1 above (simply replace mpiech/s2i-clojure with s2i-clojure in the oc import-image step and you’ll be using your newly created builder.

You can also use the s2i/Docker combination outside OpenShift to use the builder to make an app like this:

    s2i build https://github.com/mpiech/cpjhello s2i-clojure sample-app --incremental
    docker run -p 8080:8080 sample-app

And you should see your Hello World at http://localhost:8080.

A couple of other blog posts I found helpful in getting my head around S2I are https://blog.openshift.com/chaining-builds/ by Jorge Morales and https://blog.openshift.com/multiple-deployment-methods-openshift/ by Laurent Broudoux just in case you’re looking for additional depth.

Ok, hack away!

Categories
OpenShift Container Platform, OpenShift Dedicated, OpenShift Ecosystem, OpenShift Online, OpenShift Origin, Products, Technologies
Tags
, , ,
  • pilhuhn

    Awesome, thanks for sharing!