At Contrast, we like to enable developers to solve their own problems without submitting tickets to the Operations team. We also like to define our infrastructure with code.
I'll show you a new continuous integration strategy, which enables projects to bring their build dependencies with them as code and avoids the configuration overhead that’s typically associated with setting up a new automated build. To accomplish this, we’ll use Docker to run builds inside project-specific Docker containers. All the configuration to do this will reside right next to our code.
Most software projects require some initial setup before a developer can run a build. For example, to build a typical Java web project, we must first install and configure Java, Maven, node.js, etc. Let's call the set of programs we need to build a project the "build dependencies". Developers usually do a good job of updating projects' README documents with instructions on how to set up these build dependencies, and we hope that we all install roughly the same versions. In addition to developers' machines, the continuous integration server needs to be able to build our projects, so it also needs access to machines that have the right build dependencies installed.
We recently started a new project, ts-benchmark, which introduces a new set of build dependencies that we haven’t used in other projects: Go and its associated build tools. To set up the continuous integration server with the right build dependencies for this new project, we usually need to get in touch with our Operations team about building a new Go build node for building Go projects. If we ever change the build dependencies for this project, we need to work with Operations again to configure a new build machine. Furthermore, if we introduce new build dependencies in a feature branch, and these dependencies are incompatible with our old build machine (e.g. upgrade our version of Go), we may need to maintain a new build machine and a new build configuration just for the feature branch.
We can do better – so, we introduced a generic build machine that simply has Docker installed. This is how we use it to build our new project.
We want projects to "bring their build dependencies with them as code". To do this, we include a Dockerfile in the source code repository, which instructs Docker how to build an image that contains all the project's build dependencies. Here's the Dockerfile for building the ts-benchmark project:
FROM golang |
In case you're not familiar with Dockerfiles, let’s break down these two lines.
Remember – the goal is to build our project using a Docker container that contains all the necessary build dependencies. For those of you who aren’t familiar with Docker, let’s quickly review some terminology:
The Dockerfile defines how to create a Docker image with all the build dependencies installed. We’ll then use a shell script to build our Docker image, and run the build in a Docker container made from that image. Let's break down an example shell script for the ts-benchmark project:
#!/bin/bash -eu docker build -t tsbenchmark-build . docker run --rm -v $(pwd):/go/src/contrast/tsbenchmark:rw -w /go/src/contrast/tsbenchmark --entrypoint /usr/bin/make tsbenchmark-build linux |
So, `docker run` executes `make linux` inside of a new Docker container which contains all our build dependencies. Since our project directory is mapped to the Docker container file system with the volume option, the resulting artifacts from executing `make linux` are available where we expect them to be – on disk, as if we ran `make linux` without Docker!
The goal is to enable projects to bring their build dependencies with them as code. This enables developers to solve their own problems without help from Operations, tracks our infrastructure in version control and allows branches of our code to use different build dependencies without any burden on the continuous integration system. To do this, projects include a Dockerfile, which instructs Docker how to build an image that contains all the necessary build dependencies. The projects' build scripts run the build inside of a Docker container created from the Docker image. If you're unfamiliar with Docker, this basic use case is a great way to start learning about the platform and how we use it at Contrast.
Get the latest content from Contrast directly to your mailbox. By subscribing, you will stay up to date with all the latest and greatest from Contrast.