Designing a Source-to-Image build for a Go application

Designing a Source-to-Image build for a Go application

In the second article in this series on S2I for Golang, learn how to prepare an environment specifically for a Go application.

gopher illustrations
Image credits : 
x

Get the newsletter

Join the 85,000 open source advocates who receive our giveaway alerts and article roundups.

In my first article in this series about Source-to-Image (S2I), we examined the required files and discussed how the S2I standard works with any programming language, from Python to Ruby to Go. Now let's explore designing an S2I build specifically for a Go application. A disclaimer: I still like to call Go "Golang" even though it's not officially called that.

Golang S2I files

The heart of all S2I environments begins with the configuration file defined as a Dockerfile. Our Golang builder image is no exception; this project will need a Dockerfile to create the builder, an assemble script with the logic to compile a Go application, a run script to launch the app (for convenience only), and a save-artifacts script to save the compiled application.

Let's take a look at these required files, as we will use them for the builder image.

Note: All the referenced files are available in my golang-s2i GitHub repository.

Dockerfile

This Dockerfile is not very complicated. This builder image will use the upstream golang:1.12 image as its base so we will not have to install Go and set up the environment. Two environment variables must be set to allow Go applications to run in a container environment:

  • CGO_ENABLED=0
  • GOOS=linux

If you want to know more about why they are used, check out Kelsey Hightower's excellent article "Building Docker images for static Go binaries."

The GOCACHE=/tmp environment variable also needs to be set in the Dockerfile to avoid write errors when running the build as a user other than root. It is an OKD/OpenShift convention to support running containers with arbitrary UIDs, but it's also just good practice to avoid needing root privileges for builds as well. Check out Dan Walsh's article "Just say no to root (in containers)" for more about why this is a good thing.

Then set two more environment variables:

  • STI_SCRIPTS_PATH=/usr/libexec/s2i
  • SOURCE_DIR=/go/src/app

The first tells S2I where to find the scripts it needs to run, and the second is just for convenience so our project is DRY.

In the next section, COPY the S2I scripts (see below) to the builder image, create and chmod the $SOURCE_DIR where the scripts will compile the application, and set this as the WORKDIR so that subsequent operations occur in that directory:

COPY ./s2i/bin/ ${STI_SCRIPTS_PATH}
RUN mkdir -p $SOURCE_DIR \
      && chmod 0777 $SOURCE_DIR
WORKDIR $SOURCE_DIR

Note: The $SOURCE_DIR is set to /go/src/app because the $GOPATH variable in the parent golang:1.12 image is set to /go.

Finally, set USER 1001 to drop root privileges and assure support for random UIDs, as mentioned above, and set the CMD to the S2I usage script:

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

At this point, our Dockerfile should look something like the Dockerfile in my GitHub repo, except for a few labels and comments.

Assemble

S2I uses the assemble script to compile the Go app.

When S2I copies the application code into the builder image, it places it into /tmp/src. Since the upstream image sets the GOPATH to /go, the assemble script just needs to copy to a directory in there: the $SOURCE_DIR we set earlier in the image. Then it is just a matter of running go get and go build. Because the WORKDIR was set to the same directory, the script will run from there:

#!/bin/bash -e

# Copy the src to the current directory - the WORKDIR/$SOURCE_DIR.
cp -Rf /tmp/src/. ./
go get -v
go build -v -o app -a -installsuffix cgo

The go build command is run with the -o app build flag so that the resulting binary will have a predictable name (specifically, "app").

This is all that the assemble script requires, but go test -v can also be included at the end of the script to make sure the application passes all its code tests (because it will make the image build fail if any tests fail).

That's it for the assemble script. The assemble script in GitHub includes a few commands for copying artifacts from a previous build, but it is otherwise the same.

run

S2I uses the run script to execute the app if it's running a container from the resulting image build. Appropriately, it is just two lines:

#!/bin/bash
exec app

Note: If an application requires arguments, this script would need to be tweaked a little to pass those arguments (and is why the script is included at all for Go, when the app itself could just be the "run" script).

save-artifacts

The save-artifacts image is not required for S2I. Its purpose is to reuse artifacts from a previous build (think: downloaded PIP packages or Ruby gems) and to do incremental builds so it can make building images in development much faster. The Golang builder image will use it a bit differently: to allow us to extract the compiled binary file so it can be included in a much slimmer runtime image (see below).

S2I expects the save-artifacts script to take all the files and dependencies for an application and stream them through the tar command to stdout so they can be received on the other end and saved. This is easy in theory but can be tricky if you're not careful. The contents streamed to stdout must include ONLY the contents of the tar file; we must be careful to prevent text or other content from being sent. We need to pipe all output other than that of tar to /dev/null to ensure the tar archive is not corrupted with other data.

Pro tip: If we attempt to stream the contents of the tar file out of the container manually by running the save-artifacts script as the command for the container, we MUST NOT use the -i or -t arguments because tar refuses to stream output to the pseudo-terminal.

In our case, because the builder is compiling a single binary, this script is also just two lines and has no other output to worry about:

#!/bin/sh -e
tar cf – app

We now have a fully functional S2I environment for a Golang application. We can play around with this on the command line. If you need a refresher on all these files, go back and review the first article in this series. In the next article, we will continue with the build process and go through the workflow of building out a Go application.

About the author

Chris Collins
Chris Collins - Chris Collins is a senior automation engineer and the web architecture lead at Duke University’s Office of Information Technology. He’s a container and automation evangelist, helps leads adoption of containers within the university, and loves to talk about them with anyone who will listen, much to the annoyance of the co-workers who sit closest to him.