Introduction
Two of the major requirements for a Continuous Integration/Delivery system (CI/CD) are:
- Isolation: the ability to run a well isolated environment from other builds executed on the same infrastructure;
- Reproducibility : the ability to reproduce the build environment from a pre-defined setup.
These requirements also apply to local development environments, ensuring developers the ability of consistently reproduce building and testing processes.
A great way to bootstrap reproducible and isolated environments is using Docker containers.
“Docker containers wrap a piece of software in a complete filesystem that contains everything needed to run: code, runtime, system tools, system libraries – anything that can be installed on a server. This guarantees that the software will always run the same, regardless of its environment.”
Compared to a Virtual Machine (VM), Docker it’s faster to launch and lighter to use. The Docker image can be defined both as a binary image pulled from a repository, or as a plain text Dockerfile that can be stored along side the project source, so that the source and environment are always in sync and recorded.
Docker Builds
Following the Software Structure and Software Automation guides, we can easily add a Docker-based building system to a project in such a way that can be used both manually and automatically via CI/CD:
1. Dockerfile
“resources/DockerDev/Dockerfile
”
This file defines the base Docker development environment. It can be based on a minimum standard image and define all the required dependencies, or it can inherit from a custom Docker image that can be shared across multiple software projects. In any case we can add here any extra dependency and configuration.
Projects like tecnickcom/alldev can be created to generate custom Docker images to be reused across various projects. Each new image is tagged with the version number and the “latest” tag.
Example
FROM phusion/baseimage
MAINTAINER name.surname@example.com
ENV DEBIAN_FRONTEND noninteractive
ENV TERM linux
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
ENV HOME /root
ENV DISPLAY :0
ENV GOPATH=/root
ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH
# Install extra packages
RUN apt-get install -y build-essential
# Install and configure GO
RUN wget https://storage.googleapis.com/golang/go1.7.3.linux-amd64.tar.gz && \
tar xvf go1.7.3.linux-amd64.tar.gz && \
mv go /usr/local && \
mkdir -p /root/bin && \
mkdir -p /root/pkg && \
mkdir -p /root/src && \
echo 'export GOPATH=/root' >> /root/.profile && \
echo 'export PATH=/usr/local/go/bin:$GOPATH/bin:$PATH' >> /root/.profile && \
go version
2. Building Script
“dockerbuild.sh
”
This script is used to generate a temporary Docker container to build and test the software and copy the results back in the project target folder.
#!/bin/sh
#
# dockerbuild.sh
#
# Build the software inside a Docker container
#
# @author Nicola Asuni <info@tecnick.com>
# @copyright 2015-2016 Nicola Asuni - Tecnick.com LTD
# ------------------------------------------------------------------------------
# NOTES:
# This script requires Docker
# EXAMPLE USAGE:
# CVSPATH=project VENDOR=vendorname PROJECT=projectname MAKETARGET=buildall ./dockerbuild.sh
# Get vendor and project name
: ${CVSPATH:=project}
: ${VENDOR:=vendor}
: ${PROJECT:=project}
# make target to execute
: ${MAKETARGET:=buildall}
# Name of the base development Docker image
DOCKERDEV=${VENDOR}/dev_${PROJECT}
# Build the base environment and keep it cached locally
docker build -t ${DOCKERDEV} ./resources/DockerDev/
# Define the project root path
PRJPATH=/root/src/${CVSPATH}/${PROJECT}
# Generate a temporary Dockerfile to build and test the project
# NOTE: The exit status of the RUN command is stored to be returned later,
# so in case of error we can continue without interrupting this script.
cat > Dockerfile <<- EOM
FROM ${DOCKERDEV}
RUN mkdir -p ${PRJPATH}
ADD ./ ${PRJPATH}
WORKDIR ${PRJPATH}
RUN make ${MAKETARGET} || (echo \$? > target/make.exit)
EOM
# Define the temporary Docker image name
DOCKER_IMAGE_NAME=${VENDOR}/build_${PROJECT}
# Build the Docker image
docker build --no-cache -t ${DOCKER_IMAGE_NAME} .
# Start a container using the newly created Docker image
CONTAINER_ID=$(docker run -d ${DOCKER_IMAGE_NAME})
# Copy all build/test artifacts back to the host
docker cp ${CONTAINER_ID}:"${PRJPATH}/target" ./
# Remove the temporary container and image
docker rm -f ${CONTAINER_ID} || true
docker rmi -f ${DOCKER_IMAGE_NAME} || true
3. Makefile Target
“dbuild
”
Following the model proposed in the Software
Automation guide, we can use the “make dbuild
”
command to execute the building script both manually or via CI/CD
task.
# Full build and test sequence
buildall: deps build qa
# Build everything inside a Docker container
dbuild:
@mkdir -p target
@rm -rf target/*
@echo 0 > target/make.exit
CVSPATH=$(CVSPATH) VENDOR=$(VENDOR) PROJECT=$(PROJECT) MAKETARGET='$(MAKETARGET)' ./dockerbuild.sh
@exit `cat target/make.exit`
In this example we are returning the exit code of the
“make buildall
” command.
An arbitrary make target can be executed inside a Docker
container by specifying the MAKETARGET
parameter:
MAKETARGET='qa' make dbuild
The list of make targets can be obtained by typing
make
.
Useful Docker Commands
To manually create a container you can execute:
docker build --tag="example/devenv" ./resources/DockerDev/
where:
example/devenv
is name of the Docker image./resources/DockerDev/
is the path to the directory containing the Dockerfile
To log into the newly created container:
docker run -t -i example/devenv /bin/bash
To get the container ID:
CONTAINER_ID=`docker ps -a | grep example/devenv | cut -c1-12`
To delete the newly created docker container:
docker rm -f $CONTAINER_ID
To delete the docker image:
docker rmi -f example/devenv
To delete all containers
docker rm $(docker ps -a -q)
To delete all images
docker rmi $(docker images -q)