At Addteq, some of the projects that we build with Bamboo require the build system to have certain specialized software, such as the Atlassian SDK installed. To accomplish this, we install the software on the machine where Bamboo is running, or set up remote build agents on other machines that have the software. However, this can become a problem when different projects require conflicting sets of software (such as different versions of the Atlassian SDK), or earlier versions of software than what’s already in use on a machine, and there aren’t enough machines to dedicate to builds. To solve this, we create remote agents with different sets of software and run them all on the same machine with Docker. Docker allows us to create images of Linux systems, customized in any way one would normally customize Linux, which can then run as self-contained instances or containers. The only limit on the number of containers for images is resources, as the process is inherently scalable ad infinitum.
A Docker image is constructed from a Dockerfile, which consists of the name of a base image (most often a version of Debian or Ubuntu), a series of commands to run in the base image to customize it, and a CMD line specifying the default command to run when the resulting image is started as a container. For example:
FROM ubuntu:14.04 RUN apt-get update && apt-get install -y git maven openjdk-7-jdk openjdk-7-jre-headless RUN git config --global --add user.name 'Docker remote Bamboo agent' RUN git config --global --add user.email email@example.com COPY atlassian-bamboo-agent-installer.jar /root CMD java -jar /root/atlassian-bamboo-agent-installer.jar $BAMBOO_SERVER/agentServer/
By saving this in a file named “Dockerfile” and running “docker build -t custom-agent .” in the directory containing the file and, in this case, also containing atlassian-bamboo-agent-installer. jar, we create a Docker image named “custom-agent” based on Ubuntu that contains Git, Maven, Java, and a Bamboo remote agent. The ubuntu:14.04 base image is available on the Docker registry and will be downloaded automatically during the image build process if it is not already on the local machine. (Atlassian provides a remote agent Docker image of its own, but it is based on its own custom version of Ubuntu with contents about which they are not forthcoming, so we made our own.) At each subsequent line of the Dockerfile, Docker runs a command inside this image or otherwise modifies it until at the end we have an image to our liking. RUN runs a command inside the base image just as if we were running it from the command line (specifically, in /bin/sh. Note that each command is run in a separate shell, so RUN cd /path and RUN export VAR=val won’t affect subsequent RUN commands; instead do WORKDIR /path and ENV VAR val). COPY copies a file (in this case the Bamboo remote agent JAR) from the local directory containing the Dockerfile into the image. CMD specifies the default command to run when a new container is created from the image. The command used here makes use of an environment variable that will not be set until we run the image.
When we run docker build, Docker doesn’t just store the final image but also stores the image as it existed after each command in its build cache. These intermediate images are called layers. When we run docker build again, Docker compares each command against the ones that created the layers in the build cache. If the combination of the current image state plus the next Dockerfile command (including the contents of COPYed files) has already been done before, Docker will use the cached result rather than running the command again. However, these cached layers are remembered only as long as they’re part of an image stored on the local machine. If we use docker rmi to delete all images that have a given layer in their build history, the layer will be deleted from the build cache as well. As a result, by structuring Dockerfiles appropriately, users can cut down on the time it takes to rebuild the image whenever the file is modified. Best practices include: placing time-intensive commands that are unlikely to change (like compiling a specific version of a program from source) at the top of the Dockerfile, while placing quicker
commands that are likely to change often (like copying in a list of hosts to interact with) at the bottom.
If we create several images that differ only slightly, like in the version of the primary software, we can give them all the same name but different tags. Each image has a tag, usually a version number as part of its name, which is appended to the base name of the image with a colon; e.g., we can write “docker build – t image:1.0.2 .”, “FROM image:1.0.3”, and “docker run image:1.0.4”. If we don’t specify a tag when creating or using an image, the tag “latest” is implied; thus, “docker build -t image .” actually builds image:latest, “FROM image” uses image:latest as the base image, and “docker run image” runs image:latest. We used a tag in the Dockerfile above when writing “FROM ubuntu: 14.04” in order to specify the specific version of Ubuntu to build from; the available tags for an image in the Docker registry are listed on the page for that image, sometimes under the “Information” tab, always under the “Tags” tab.
The custom-agent image can be run with the command:
docker run -d -e BAMBOO_SERVER=address --name agent-container custom-agent
(If we don’t want to run the default command set in the Dockerfile, we can explicitly specify a command to run with “docker run custom-agent command args”, and we can run a shell inside our custom environment with “docker run -i -t custom-agent /bin/bash”.) The -d option causes the container to run in detached mode as a background process, with all of its output being logged. –name agent-container assigns the container a name (which must be unique among all Docker containers that currently exist on the system) that we can use to refer to the container in further Docker commands. The container can also be referred to by a hash value that is output when it starts up. (If the container is not run with -d, this value will not be output, and you will have to find the ID by finding your container among all running containers in the output of docker ps.) When the container created from the image starts, the remote agent will run inside the operating system defined by the image with access to all of the software inthe image (and only that software), and the agent will connect to the Bamboo server and perform builds for it like a normal remote agent. Depending on how Bamboo is configured, users may have to authenticate the remote agent by visiting a URL that the process outputs. The output from the container can be viewed by running “docker logs agent-container” or whatever a user named their container on their machine.
The container will run until the primary process inside stops either through exiting successfully or encountering an error or, if that doesn’t happen, until a user makes it stop by running “docker stop agent-container”. However, stopped containers continue to exist on the local machine (until it reboots, at least), though they won’t show up under docker ps unless someone adds the -a option. The idea behind keeping the container around is to allow the user to examine the stopped container, possibly copying out files with docker cp, or to restart it with docker start. If a user dosn’t need or want to examine or restart a stopped container, it can be deleted with docker rm agent-container. If a user is sure they won’t ever want to keep a container around once it stops, they can tell the container to automatically delete itself when done by including the –rm option in the docker run command.
One important feature of Bamboo agents is their capabilities, which Bamboo uses to determine which agents can perform which jobs. The remote agent can determine the values for some capabilities of a system automatically (such as the Java home directory and the location of Git), but others must be explicitly specified in a ~/bamboo-agent-home/bin/bamboo-capabilities.properties file. For example, if a Docker image has the program foo installed in /usr/bin/foo and a Maven 3 installation in the /usr /local/share/maven directory, users can make the agent aware of these by adding a bamboocapabilities.properties file to the Docker build directory with the following contents:
system.builder.command.foo=/usr/bin/foo system.builder.mvn3.Maven\ 3=/usr/local/share/maven # We can even add a custom capability -- say, a string FOO_VERSION containing the version number of the installation of `foo`: FOO_VERSION=42.17
and then adding these lines to the Dockerfile:
# The remote agent, like almost all Docker processes, runs as root, so the `bamboo-agent-home` directory must be located in /root. RUN mkdir -p /root/bamboo-agent-home/bin COPY bamboo-capabilities.properties /root/bamboo-agent-home/bin/bamboo-capabilities.properties
Docker can be used when deploying Bamboo builds as well. A Tomcat server can be created as a Docker
docker run -d -p 7000:8080 --name tomcat_server tutum/tomcat:6.0
This uses version 6.0 of the tutum/tomcat image on the Docker registry and sets it to listen on the local machine’s port 7000. The admin password for the server can be found by inspecting its output with “docker logs tutum_server”, after which Bamboo can deploy web apps to the server like it would deploy to any other Tomcat instance.
In short, Docker is useful for automating the shipping of software, avoiding issues between parts of cross functional teams that may have their tools our of sync, providing speed and flexibility for builds, and allowing for infrastructure agnosticism. Of course, users should take the time to learn the tool and all of its command line arguments to take advantage of all of its features. However, as Docker gains traction, it will be increasingly supported by higher level tools. For example, Atlassian’s Bamboo, introduced support for Docker agents in version 5.7 in November of 2014, and Docker tasks in version 5.8 this March (2015). Atlassian has made numerous commitments to increasing its support of Docker as well. However, as a group of ‘hardcore coders’ we still recommend learning what’s going on under the hood as the integration process continues. Regardless, Docker remains a great choice as a compliment to a team’s build pipeline.