Overview

Deploying Pull Requests with Docker

No Comments

The Git repositories in my current project are hosted on Bitbucket Cloud. Any code changes have to go through pull requests. Jenkins builds the pull requests and gives its approval if the build is green. Additionally, at least one team member carries out a code review. Though a code review does not generally mandate that a reviewer build and test the app locally, one sometimes does want to see the result of the code changes, especially when, e. g., CSS/HTML are affected. In such cases, a visual test can help to understand and verify the changes.

In order to make things as easy as possible for the reviewer, we wanted to find a way to make a pull request testable with just the click of a button. We’ve just recently migrated our application to Spring Boot. Currently, we are still building a war file that is deployed on Tomcat. However, the executable war file can be run directly with the embedded Tomcat and, thus, can quite easily be packaged into a Docker image.

MongoDB is used as database. For the local development we dockerize the database. What’s special about that, is that the Docker image already contains data. The nature of our application requires that the database always have current data. For that purpose we have a nightly job that generates test data for the integration instance of our MongoDB. Jenkins in turn builds a Docker image for the MongoDB every night. The job creates a dump from the integration instance and imports it into the Docker image. The image is pushed to the Docker registry. Every morning, the developers can then pull a fresh image.

Our Jenkins also runs on Docker, one container for the master and another one for the build slave. The host’s Docker socket is mounted into the slave container. Now, when Jenkins builds a pull request, it also builds a Docker image thereof, which is tagged with the pull request number. Via Build Pipeline Plugin it is now possible to deploy a pull request together with a fresh MongoDB instance on the Jenkins host.

PR Pipeline

The job for building the pull request has a manual downstream job (part of the Build Pipeline Plugin) which deploys app and database.

postbuild-1

A Python script creates a Docker network and starts the containers.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
import socket
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
from subprocess import Popen, STDOUT, PIPE
 
 
def docker(*arguments):
    cmdline = ['docker']
    cmdline.extend(arguments)
 
    print()
    print(' '.join(arg for arg in cmdline))
 
    proc = Popen(cmdline, stdout=PIPE, stderr=STDOUT)
    output = []
    while True:
        line = proc.stdout.readline()
        if not line:
            break
 
        line = line.rstrip()
        print('docker> %s' % line)
        output.append(line)
 
    proc.wait()
    return output
 
 
def start_myproject():
    print('Starting MyProject: %s' % version)
 
    docker('network', 'create', '-d', 'bridge', network_name)
 
    docker('run', '-d', '--name', mongo, '--network', network_name, '--network-alias', mongo, '-P', 'myrepo/mongo')
 
    docker('run', '-d', '--name', myproject, '--network', network_name, '-P', 'myrepo/myproject:%s' % version,
           '--spring.profiles.active=dev', '--database.hosts=%s' % mongo)
 
    docker('port', mongo)
    docker('port', myproject)
 
 
def destroy_myproject():
    print('Destroying MyProject: %s' % version)
 
    docker('stop', myproject)
    docker('rm', myproject)
 
    docker('stop', mongo)
    docker('rm', mongo)
 
    docker('network', 'rm', network_name)
 
 
args_parser = ArgumentParser(description='Manages MyProject/Docker dev deployments')
sub_parsers = args_parser.add_subparsers()
 
start_parser = sub_parsers.add_parser('start', help='Start MyProject and Mongo')
start_parser.add_argument('--version', '-v', required=True, help='The MyProject version to start')
start_parser.set_defaults(func=start_myproject)
 
destroy_parser = sub_parsers.add_parser('destroy', help='Destroy MyProject and Mongo')
destroy_parser.add_argument('--version', '-v', required=True, help='The MyProject version to destroy')
destroy_parser.set_defaults(func=destroy_myproject)
 
args = args_parser.parse_args()
 
version = args.version
network_name = version
mongo = 'mongo_%s' % version
myproject = 'myproject_%s' % version
 
args.func()

shell-start

The port for the application is automatically assigned by Docker. The URL is printed to the log of the job and can be quickly accessed from the build pipeline view. The job gets the pull request number as parameter and thus knows which image to start.

The deployment job has yet another manual downstream job for destroying the deployment.

postbuild-2

shell-destroy

In order to get quick access to application logs or to open a shell on a container without having to ssh into the Jenkins host, we installed Shipyard. This gives a good overview over running containers and allows you to destroy obsolete forgotten ones.

Why no Pipeline Job?

Unfortunately, the Bitbucket Pull Request Builder Plugin is not compatible with the new Jenkins pipelines. There is a JIRA ticket for this which refers to the Bitbucket Branch Source Plugin. Unfortunately, that’s out of question for us because it uses webhooks. For the master build, however, we do already use a pipeline job.

 

Reinhard Nägele

Reinhard is a Senior IT Consultant at codecentric’s Munich office. He can look back on about 18 years in Java development. In his projects, he is a strong proponent of automation. In recent years, he has gained substantial knowledge in infrastructure topics around Maven, Git, Jenkins, Docker, and has also spread his knowledge in trainings. Currently, he is busy looking into cluster platforms such as Kubernetes and DCOS and is excited about the new possibilities they offer. Reinhard enjoys contributing to open-source projects. He is a member of the maintainers team for Kubernetes Helm Charts.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Comment

Your email address will not be published. Required fields are marked *