Building a custom S2I image in OpenShift

Source to image(s2i) is the most flexible deployment strategy in OpenShift. Learn how to build a custom s2i from an Ubuntu base image.

We did a deep dive on how s2i images work. Now, we will look at a more elaborate example of s2i and build our own from scratch. We will be building an Ubuntu based image which runs PHP 7.1 through PHP FPM and uses Nginx as the webserver instead of Apache.

The s2i image

We start with the Dockerfile and the LABEL directive.

LABEL io.k8s.description="Base image for Ubuntu based LEMP" \
      io.k8s.display-name="OpenShift LEMP" \
      io.openshift.s2i.scripts-url="image:///usr/libexec/s2i" \
      io.openshift.expose-services="8080:http" \
      io.openshift.tags="builder, Nginx, php-fpm, php-7.1"

The “io.openshift.s2i.scripts-url” indicates where in the image the s2i scripts will be found. The s2i files are copied later to that path.

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

It is important that we create a non root user and change ownership to all the files involved to that user.

RUN useradd -u 1001 -r -g 0 -d ${HOME} -s /sbin/nologin -c "Default Application User" default \
    && mkdir -p ${HOME} \
    && chown -R 1001:0 ${HOME} && chmod -R g+rwX ${HOME}

I copy an Nginx configuration specific to the PHP application I’m running. You can customize this or modify this to load from a preconfigured location in your source code.

COPY ./files/symfony-demo /etc/nginx/sites-enabled/default

Though I’ve sort of hardcoded the Nginx configuration here, I’d recommend to go with the latter approach to make your s2i image more reusable. For instance, someone running Magento on Nginx can reuse this image by just placing their Nginx configuration in the prespecified location.

I do some housekeeping finally, where I assign the working directory to /opt/app-root/src, pointed to ${HOME} here. I run Nginx in a non standard port(other than 80 as it can be run in 80 only as root).

WORKDIR ${HOME}

EXPOSE 8080

USER 1001

Finally, this is an s2i image, not the final one, so I just print the usage script and exit.

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

The s2i scripts

The first script which gets fired is the “assemble” script.  The assemble script tries to restore artifacts stored from a previous build to reduce build time. This is not mandatory, but speeds up builds substantially if you use it right. In case of PHP, it might be the vendor directory which stores the composer libraries.

if [ "$(ls /tmp/artifacts/ 2>/dev/null)" ]; then
  echo "---> Restoring build artifacts..."
  mv /tmp/artifacts/. ./
fi

Which brings us to the next script, the “save-artifacts” script. This is just expected to emit a tar archive as output to work correctly.

#!/bin/sh
pushd ${HOME} >/dev/null
tar cf - vendor
popd >/dev/null

The “usage” script just prints a brief info about the s2i image. This can be anything streamed to stdout.

The last and probably most important script is the “run” script. This runs the actual process of the container in the foreground.

#!/bin/bash

echo " -----> Run HTTP server."
/usr/sbin/php-fpm7.1 --force-stderr --daemonize --fpm-config /etc/php/7.1/fpm/pool.d/www.conf
exec nginx -g "daemon off;"

In our case, this will be Nginx, and also php-fpm. I choose to run php-fpm in the background and Nginx in the foreground. The logs will be emitted to stdout. Here, I’m potentially violating an important but unwritten rule abour containers, but running more than 1 process inside the container, but that’s a story for another day.

Build the s2i image

Let’s build this s2i image.

$ docker build -t <your-namespace>/ubuntu-lemp-s2i .

Let’s test the image real quick. First clone the PHP application source code. For this demo, we will be using Symfony’s demo project.

$ git clone git@github.com:symfony/demo.git

We use the s2i executable to build an image with “lakshminp/ubuntu-lemp-s2i” as the base and the source code of the above demo application superimposed on it.

$ s2i build ~/demo lakshminp/ubuntu-lemp-s2i test-ubuntu-lemp-s2i -e APP_ENV=prod -e APP_SECRET=foobar123 -e DATABASE_URL=sqlite:///%kernel.project_dir%/data/database.sqlite

We pass extra environment variables needed to run the app using the “-e” arguments. This newly minted image can be accessed by running a container instance of it.

$ docker run -d -p 8080:8080 test-ubuntu-lemp-s2i

Move the s2i image to OpenShift

At the time of writing this, you can’t use docker images directly from your local into your OpenShift cluster. You have to push it to a registry first.

$ docker push <your-namespace>/ubuntu-lemp-s2i

And then import the image into your OpenShift cluster. More on images and imagestreams in a later post.

$ oc import-image lakshminp/ubuntu-lemp-s2i --confirm

This command imports “lakshminp/ubuntu-lemp-s2i” from Docker’s public registry into OpenShift’s builtin registry. You can confirm this by running,

$ oc get is

Create new app using Ubuntu s2i

Now, let’s create a new app using this s2i image. We will use the same demo application as before.

$ oc new-app ubuntu-lemp-s2i~https://github.com/symfony/demo.git --name=symfony-demo-1 -e APP_ENV=prod -e APP_SECRET=foobar123 -e DATABASE_URL=sqlite:///%kernel.project_dir%/data/database.sqlite

Chasing the logs, we will find that eventually, the application building step fails.

$ oc logs -f bc/symfony-demo-1

oc build app fails
oc build app fails

To ensure that environment variables are available for the build image, you have to add them using the –build-env argument.

$ oc new-app ubuntu-lemp-s2i~https://github.com/symfony/demo.git --name=symfony-demo-2 --build-env APP_ENV=prod --build-env APP_SECRET=foobar123 --build-env DATABASE_URL=sqlite:///%kernel.project_dir%/data/database.sqlite

Now the build will work fine. If you don’t understand what a build image, then read the section under “The OpenShift build config resource” in this post.

To expose the app,

$ oc expose svc/symfony-demo-2
Ubuntu PHP 7.1 Nginx demo app
Ubuntu PHP 7.1 Nginx demo app

If you noticed, we didn’t add any deployment or devops related modifications in our source code. This is one of the reasons s2i is touted as one of the best build strategies in OpenShift. Your team needn’t have any context about the deployment or infrastructure at all to deploy their code.

In the next posts, we will be doing further comparisons of OpenShift vs Kubernetes, and exploring many other features of OpenShift like monitoring, persistent storage and routing.