Build an app image with s2i
Tags: docker, devops, openshift, s2i
Published: 2020-07-19 17:45:15

## What is `s2i`?

`source-to-image`, or `s2i` as commonly abbreviated, is a build tool that allows an app developer and/or operator the ability to take the app source code and construct an OCI image (aka docker image, aka container image).

### Concepts

##### Builder

A builder is an image that knows how to build a specific type of source code. Although it would be possible for a single builder to support multiple languages it is common that a builder only builds one language family.

The following publically available builders are available:
- [OpenShift Builders](https://github.com/sclorg/?q=s2i--container&type=&language=shell)
- [Community Builders](https://github.com/openshift-s2i/)
- [fabric8 Builders](https://github.com/fabric8io-images/s2i)

##### Build image

The base image in which the build process is executed.

##### Run image

The base image in which the built application runs.

### Features

##### Separation of concerns

By using a tool such as `s2i`, the app developer can focus on adding features and maintaining the application. On the other side, DevOps can ensure that the application is built in a secure and reproducible manner by providing builders that should be used by developers.

##### Minimal to no configuration

In most cases, no additional `s2i` configuration is necessary and it's able to build the application. In the cases where a more complex application needs to be built there are various configuration points (to be covered in a separate tutorial).

### Process

The process, as well defined in the project [README.md](https://github.com/openshift/source-to-image#build-workflow):

> The `s2i build` workflow is:
> 
> - `s2i` creates a container based on the build image and passes it a tar file that contains:
>     - The application source in src, excluding any files selected by `.s2iignore`
>     - The build artifacts in `artifacts` (if applicable - see incremental builds)
> - `s2i` sets the environment variables from `.s2i/environment` (optional)
> - `s2i` starts the container and runs its assemble script
> - `s2i` waits for the container to finish
> - `s2i` commits the container, setting the CMD for the output image to be the run script and tagging the image with the name provided.

## Install

Now that we've got some idea on what `s2i` is and why we might want to use it let's start by installing it.

You may find the latest releases [here](https://github.com/openshift/source-to-image/releases).

These are the command I ran to install it in `/usr/local/bin/` on a macOS:

```shell
curl -sSL https://github.com/openshift/source-to-image/releases/download/v1.3.0/source-to-image-v1.3.0-eed2850f-darwin-amd64.tar.gz -o /tmp/s2i.tgz
tar -xvf /tmp/s2i.tgz -C /usr/local/bin/
s2i version
```

## Build

Next, we'll build the app. 

Let's use Spring's sample project, [petclinic](https://github.com/spring-projects/spring-petclinic), as the source.

```shell
s2i build https://github.com/spring-projects/spring-petclinic fabric8/s2i-java pet-clinic
```

#### Breakdown

If we breakdown the command, this is what it all means:

```shell
s2i                                             
build                                                 # command
https://github.com/spring-projects/spring-petclinic   # source (in)
fabric8/s2i-java                                      # builder image (in)
pet-clinic                                            # image name (out)
```

#### Output

The [truncated] output is as follows. Nothing spectacular to look at but we do see that it built our image.

```text
==================================================================
Starting S2I Java Build .....
S2I source build for Maven detected
Using MAVEN_OPTS '-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=40 -XX:+ExitOnOutOfMemoryError'
Found pom.xml ...
Running 'mvn -Dmaven.repo.local=/tmp/artifacts/m2 package -DskipTests -Dmaven.javadoc.skip=true -Dmaven.site.skip=true -Dmaven.source.skip=true -Djacoco.skip=true -Dcheckstyle.skip=true -Dfindbugs.skip=true -Dpmd.skip=true -Dfabric8.skip=true -e -B '
Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-17T18:33:14Z)
Maven home: /opt/maven
Java version: 1.8.0_252, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre
Default locale: en_US, platform encoding: ANSI_X3.4-1968
OS name: "linux", version: "4.19.76-linuxkit", arch: "amd64", family: "unix"
[INFO] Error stacktraces are turned on.
[INFO] Scanning for projects...
...
(Downloading a bunch of dependencies)
...
[INFO] Building jar: /tmp/src/target/spring-petclinic-2.3.1.BUILD-SNAPSHOT.jar
[INFO]
[INFO] --- spring-boot-maven-plugin:2.3.1.RELEASE:repackage (repackage) @ spring-petclinic ---
[INFO] Replacing main artifact with repackaged archive
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 06:43 min
[INFO] Finished at: 2020-07-19T16:00:45Z
[INFO] ------------------------------------------------------------------------
Copying Maven artifacts from /tmp/src/target to /deployments ...
Running: cp -v *.jar /deployments
'spring-petclinic-2.3.1.BUILD-SNAPSHOT.jar' -> '/deployments/spring-petclinic-2.3.1.BUILD-SNAPSHOT.jar'
Checking for fat jar archive...
Found spring-petclinic-2.3.1.BUILD-SNAPSHOT.jar...
... done
Build completed successfully
```

#### Pro-tip: Incremental Build

If you were to run the `build` command again you would notice that the build does EVERYTHING again. This includes downloading dependencies. By default, `s2i` aims to provide a safe reproducible build. If you'd like to leverage caching of such resources (dependant on builder implementation) you may enable incremental builds via `--incremental`.

```shell
s2i build https://github.com/spring-projects/spring-> petclinic fabric8/s2i-java pet-clinic --incremental
```


## Run

Now that we've built our image we can run it just like we normally would any other image:

```shell
docker run -p 8080:8080 pet-clinic
```

> We are binding port `8080` since that's the port our app uses by default.

#### Output

Here's the output of the petclinic app starting...

```text
Starting the Java application using /opt/run-java/run-java.sh ...
exec java -javaagent:/opt/jolokia/jolokia.jar=config=/opt/jolokia/etc/jolokia.properties -javaagent:/opt/prometheus/jmx_prometheus_javaagent.jar=9779:/opt/prometheus/prometheus-config.yml -XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=40 -XX:+ExitOnOutOfMemoryError -cp . -jar /deployments/spring-petclinic-2.3.1.BUILD-SNAPSHOT.jar
I> No access restrictor found, access to any MBean is allowed
Jolokia: Agent started with URL http://172.17.0.2:8778/jolokia/


              |\      _,,,--,,_
             /,`.-'`'   ._  \-;;,_
  _______ __|,4-  ) )_   .;.(__`'-'__     ___ __    _ ___ _______
 |       | '---''(_/._)-'(_\_)   |   |   |   |  |  | |   |       |
 |    _  |    ___|_     _|       |   |   |   |   |_| |   |       | __ _ _
 |   |_| |   |___  |   | |       |   |   |   |       |   |       | \ \ \ \
 |    ___|    ___| |   | |      _|   |___|   |  _    |   |      _|  \ \ \ \
 |   |   |   |___  |   | |     |_|       |   | | |   |   |     |_    ) ) ) )
 |___|   |_______| |___| |_______|_______|___|_|  |__|___|_______|  / / / /
 ==================================================================/_/_/_/

:: Built with Spring Boot :: 2.3.1.RELEASE


2020-07-19 16:06:40.218  INFO 1 --- [           main] o.s.s.petclinic.PetClinicApplication     : Starting PetClinicApplication v2.3.1.BUILD-SNAPSHOT on 4920727ddb55 with PID 1 (/deployments/spring-petclinic-2.3.1.BUILD-SNAPSHOT.jar started by jboss in /deployments)
2020-07-19 16:06:40.226  INFO 1 --- [           main] o.s.s.petclinic.PetClinicApplication     : No active profile set, falling back to default profiles: default
2020-07-19 16:06:42.947  INFO 1 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFERRED mode.
2020-07-19 16:06:43.168  INFO 1 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 195ms. Found 4 JPA repository interfaces.
2020-07-19 16:06:44.925  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2020-07-19 16:06:44.951  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-07-19 16:06:44.952  INFO 1 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.36]
2020-07-19 16:06:45.140  INFO 1 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-07-19 16:06:45.140  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 4797 ms
2020-07-19 16:06:46.331  INFO 1 --- [           main] org.ehcache.core.EhcacheManager          : Cache 'vets' created in EhcacheManager.
2020-07-19 16:06:46.375  INFO 1 --- [           main] org.ehcache.jsr107.Eh107CacheManager     : Registering Ehcache MBean javax.cache:type=CacheStatistics,CacheManager=urn.X-ehcache.jsr107-default-config,Cache=vets
2020-07-19 16:06:46.386  INFO 1 --- [           main] org.ehcache.jsr107.Eh107CacheManager     : Registering Ehcache MBean javax.cache:type=CacheStatistics,CacheManager=urn.X-ehcache.jsr107-default-config,Cache=vets
2020-07-19 16:06:46.498  INFO 1 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2020-07-19 16:06:47.133  INFO 1 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2020-07-19 16:06:47.609  INFO 1 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-07-19 16:06:48.127  INFO 1 --- [         task-1] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2020-07-19 16:06:49.586  INFO 1 --- [         task-1] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 5.4.17.Final
2020-07-19 16:06:50.482  INFO 1 --- [         task-1] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.1.0.Final}
2020-07-19 16:06:51.609  INFO 1 --- [         task-1] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2020-07-19 16:06:54.376  INFO 1 --- [         task-1] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2020-07-19 16:06:54.432  INFO 1 --- [         task-1] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2020-07-19 16:06:54.523  INFO 1 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 13 endpoint(s) beneath base path '/actuator'
2020-07-19 16:06:54.605  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-07-19 16:06:54.608  INFO 1 --- [           main] DeferredRepositoryInitializationListener : Triggering deferred initialization of Spring Data repositories?
2020-07-19 16:06:56.055  INFO 1 --- [           main] DeferredRepositoryInitializationListener : Spring Data repositories initialized!
2020-07-19 16:06:56.073  INFO 1 --- [           main] o.s.s.petclinic.PetClinicApplication     : Started PetClinicApplication in 16.736 seconds (JVM running for 18.197)
^C2020-07-19 16:09:43.051  INFO 1 --- [extShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2020-07-19 16:09:43.056  INFO 1 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
2020-07-19 16:09:43.057  INFO 1 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2020-07-19 16:09:43.074  INFO 1 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
2020-07-19 16:09:43.085  INFO 1 --- [extShutdownHook] org.ehcache.core.EhcacheManager          : Cache 'vets' removed from EhcacheManager.
```

#### Verify

Lastly, let's verify that our app is running by opening our browser to http://localhost:8080

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/9qsl90nzqc7tg9qau2sz.png)

That's it. With one simple command we've converted our java app source code to an OCI image.

This OCI image can then be pushed to any container registry ([DockerHub](https://hub.docker.com/), [ECR](https://aws.amazon.com/ecr/), [GCR](https://cloud.google.com/container-registry), [ACR](https://azure.microsoft.com/en-us/services/container-registry/), etc) and ran in any platform that supports OCI images such as #kubernetes.

---

## Pitfalls

Below are a few pitfalls to consider when using `s2i`. These may very well be moot points if they are resolved using the `--runtime-image` option. Something I plan to explore further in another post.

### Source leak

Depending on your requirements this may or not be an issue but given the way the basic process works the source code is persisted on the final app image. This not only increases the size unnecessarily but also exposes information that you otherwise might not have liked to be exposed.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/z9nrnnmwwc8ldzo0faj6.png)

### Build tools leak

This is very well dependent on the builder to a certain extent. `s2i build` without providing a separate **run image** from the **build image** means that all the tools necessary for building the application are carried over to the app image.

There are two immediate concerns:

1. From a security perspective, the additional build tools may increase the potential attack vectors.
2. From an optimization perspective, the image size is unnecessarily larger increasing storage space usage and transport data and time.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/bhl263y0yv4la77rwofw.png)

---

## Update

**07/22/2020:** A new post is up that goes into more detail about how to resolve these pitfalls:

{% post jromero/creating-an-s2i-builder-for-go-and-a-runtime-image-5d56 %}