Source-To-Image: Getting Started With s2i

Source-To-Image: Getting Started With s2i

Back in My OpenShift Days...

When I first began learning OpenShift two years ago, I was immediately struck by how easy it was to build really simple applications, especially when you were building in Java on JBoss or WildFly. You could literally create a project with just one command and it would generate everything you needed:


$ oc new-app openshift/jboss-eap-7/eap70-openshift~http://github.com/myuser/javacode.git --name=myjbossapp

As I've talked about in my entry on OpenShift on the command line, the new-app command creates a number of Kubernetes resources for you. The command I've used here though is slightly different. In that blog entry, I only specified an image, and a basic one at that:


$ oc new-app docker.io/nginx --name=demo-nginx

So what's different here? The difference is that I'm not only specifying an image, I'm specifying a code repository. The image that I'm specifying is not the image that will get deployed, but rather a base image upon which we will build our final image. OpenShift creates a build config resource that references both the base image and the source repo so that OpenShift knows how to build our target image. When a build is initiated, the source is pulled from the repo, compiled (it knows how to compile Java code if you are using Maven, for example), and a new image is created that extends the base image with the built code placed where it needs to be. As I'll show later, you can also specify a built binary or a directory either containing source or a built binary. If what I specify is a binary, it just skips the step of building the code.

This was all really cool. It made it so that I could easily demo OpenShift to interested parties and literally get the project built and deployed in two commands (the first step was the new-app step shown above, the second step was to kick off a build which would automatically kick off a deploy of the built image). I dug into how OpenShift was doing this, and that is when I discovered that this actually wasn't something built into OpenShift. It was actually a separate tool called s2i, which stands for source-to-image.

Introducing s2i

You can think of the s2i tool as essentially being an alternative to Dockerfiles for building Docker images. It is an open source tool, and you can find the source on github. s2i is written in Go, the same language that was used to build the kubectl and oc command line tools used for Kubernetes and OpenShift. You just need to have Docker installed if you download the binary of s2i, but you can also build it from source if you have Go installed.

Technically, s2i can work with any base image, but it is intended to work with images that are commonly called builder images, which is to say that they have code inside of them that the s2i tool uses to do its job. This is intentional, because often when you are using these builder images, how your code gets deployed is related to what base image you are using. In my above example, I used a JBoss EAP 7 image that was bundled with OpenShift. This image has some s2i code inside that knows how to build a Java war or ear project if it uses a common build tool like Maven.

A Simple Example

If you have s2i installed, you can easily see this work. For the purposes of this example, I'll deploy on a JBoss EAP 7 image pulled from Red Hat's developer registry. You can sign up for a developer account on Red Hat's developer site to get access to this registry (not to mention lots of other developer tools and resources that Red Hat gives out for free). Once you are signed up, you first need to login to the registry:


$ docker login registry.access.redhat.com
Username: openshiftninja
Password: <hidden>
Login Succeeded

Next I will pull the JBoss EAP 7 image, the same one that gets bundled with OpenShift:


$ docker pull registry.access.redhat.com/jboss-eap-7/eap70-openshift
Using default tag: latest
latest: Pulling from jboss-eap-7/eap70-openshift
9cadd93b16ff: Pull complete
4aa565ad8b7a: Pull complete
343ba5353b91: Pull complete
ed5233aa6fdd: Pull complete
62aae33b9ae2: Pull complete
959eddd043b4: Pull complete
Digest: sha256:a6b7475638629f354a3e845c41db1e80764a688df90febf7d1f906dcb204cf68
Status: Downloaded newer image for registry.access.redhat.com/jboss-eap-7/eap70-openshift:latest

I've stolen borrowed an example helloworld application for demonstrating deploying a simple app onto JBoss using s2i, and so without further ado, we can build the image with a simple command:


$ s2i build https://github.com/tellmejeff/jboss-helloworld.git registry.access.redhat.com/jboss-eap-7/eap70-openshift demo-jboss-app
Found pom.xml... attempting to build with 'mvn -e -Popenshift -DskipTests -Dcom.redhat.xpaas.repo.redhatga package --batch-mode -Djava.net.preferIPv4Stack=true '
Using MAVEN_OPTS '-XX:+UseParallelGC -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=40 -
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 19.819 s
[INFO] Finished at: 2018-07-03T02:45:32+00:00
[INFO] Final Memory: 21M/101M
[INFO] ------------------------------------------------------------------------
[WARNING] The requested profile "openshift" could not be activated because it does not exist.
Copying all war artifacts from /tmp/src/target directory into /opt/eap/standalone/deployments for later deployment...
'/tmp/src/target/helloworld.war' -> '/opt/eap/standalone/deployments/helloworld.war'
Copying all ear artifacts from /tmp/src/target directory into /opt/eap/standalone/deployments for later deployment...
...
Build completed successfully

This will take a little while, but when the build is complete, you can see the image you created is now in the Docker registry:


$ docker images
REPOSITORY                                               TAG                 IMAGE ID            CREATED              SIZE
demo-jboss-app                                           latest              6bc354b4fb09        About a minute ago   720MB

Simply running the image like we would any other Docker image is straightforward, and you will see the usual JBoss startup logging, with a message at the bottom telling us our war has been deployed (I've mapped the JBoss 8080 port to 9080 to avoid a port conflict on my host):


$ docker run -p 9080:8080 demo-jboss-app
...

  JBoss Bootstrap Environment

  JBOSS_HOME: /opt/eap

  JAVA: /usr/lib/jvm/java-1.8.0/bin/java
...
02:50:19,035 INFO  [org.jboss.as.server] (ServerService Thread Pool -- 38) WFLYSRV0010: Deployed "helloworld.war" (runtime-name : "helloworld.war")
02:50:19,035 INFO  [org.jboss.as.server] (ServerService Thread Pool -- 38)
...

Now if we curl the localhost, we can see our app running (the URL path is localhost:9080/helloworld, which sends back a meta refresh to /helloworld/HelloWorld, so I'm just skipping the refresh here):


$ curl -f -i localhost:9080/helloworld/HelloWorld
HTTP/1.1 200 OK
Connection: keep-alive
X-Powered-By: Undertow/1
Server: JBoss-EAP/7
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 88
Date: Tue, 03 Jul 2018 03:05:00 GMT


<html><head><title>helloworld</title></head><body>
<h1>Hello World!</h1>
</body></html>

As I mentioned before, you can use a git repo to specify the source, but you can also specify a directory where the source is as well. I have the repository cloned locally, so I can just run this to build the image:


$ s2i build . registry.access.redhat.com/jboss-eap-7/eap70-openshift demo-jboss-app
Found pom.xml... attempting to build with 'mvn -e -Popenshift -DskipTests -Dcom.redhat.xpaas.repo.redhatga package --batch-mode -Djava.net.preferIPv4Stack=true '
Using MAVEN_OPTS '-XX:+UseParallelGC -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=40 -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -XX:MaxMetaspaceSize=100m -XX:+ExitOnOutOfMemoryError'
Using Apache Maven 3.3.9 (Red Hat 3.3.9-2.8)
Maven home: /opt/rh/rh-maven33/root/usr/share/maven
Java version: 1.8.0_161, vendor: Oracle Corporation
Java home: /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.161-0.b14.el7_4.x86_64/jre
Default locale: en_US, platform encoding: ANSI_X3.4-1968
OS name: "linux", version: "4.4.0-119-generic", arch: "amd64", family: "unix"
[INFO] Error stacktraces are turned on.
[INFO] Scanning for projects...
[INFO] Downloading: https://repo1.maven.org/maven2/org/jboss/eap/quickstarts/quickstart-parent/7.1.0.GA/quickstart-parent-7.1.0.GA.pom
...

I can also specify just the pre-compiled war so that I can skip the compile step:


$ s2i build target registry.access.redhat.com/jboss-eap-7/eap70-openshift demo-jboss-app
Copying all war artifacts from /tmp/src directory into /opt/eap/standalone/deployments for later deployment...
'/tmp/src/helloworld.war' -> '/opt/eap/standalone/deployments/helloworld.war'
Copying all ear artifacts from /tmp/src directory into /opt/eap/standalone/deployments for later deployment...
Copying all rar artifacts from /tmp/src directory into /opt/eap/standalone/deployments for later deployment...
...
Build completed successfully

Conclusion

That's it for the first installment on s2i, but next week I'll dive into a little more detail on what s2i is doing under the hood and how you can customize its behavior.

Related Article