Exploring Continuous Delivery with Jenkins and Docker

Continuous Delivery is a DevOps practice where software is produced in short cycles, enabling software to be built, tested, and released faster and more frequently.

To help visualize the concept, the demonstration included below depicts a CD pipeline which “delivers” production-ready software while also deploying it. Through this demonstration, one should better understand the value of Continuous Delivery.

To begin, we will describe the tooling used in this demonstration.

The following pipeline uses Jenkins to build a Flask application into images, run unit tests against the app, pushes images to Docker Trusted Registry, and deploys the app in production to Docker Datacenter. Docker Datacenter & Docker Trusted Registry are part of Docker Enterprise Edition.

Docker Datacenter

Docker Datacenter is Docker's Containers-as-a-Service platform, while Docker Trusted Registry is Docker's enterprise-level image storage solution. Both are available on-premises or in the cloud. Docker Datacenter & Docker Trusted Registry make it easy to update and deploy services & applications. One can keep their private images secure by pushing them to Docker Trusted Registry. Deploying to Docker Datacenter is as simple as ”docker-compose up” or ”docker stack deploy”. The Jenkins instance in this demo is also is running on Docker Datacenter.

Dockerized Flask Application

The app consists of two Docker images. The first is a python image with the Flask application, served by gunicorn:

FROM python:3.4
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY requirements.txt /usr/src/app/
RUN pip install --no-cache-dir -r requirements.txt
COPY . /usr/src/app :

The second is nginx image which serves the UI & proxies the backend Flask container:

FROM nginx:latest
RUN rm /etc/nginx/conf.d/default.conf
COPY /static /var/www/app/static
COPY demo.conf /etc/nginx/conf.d/demo.conf

The app’s docker-compose file:

version: '3'
services:
  web:
    image: dtr.base2d.com/dockerdemo/flaskapp:master
    env_file:
      - env
    command: /usr/local/bin/gunicorn -w 2 -b :8000 app:app
    deploy:
      restart_policy:
        condition: on-failure
  nginx:
    image: dtr.base2d.com/dockerdemo/nginxapp:master
    depends_on:
      - web
    ports:
      - "8499:8499"
    deploy:
      restart_policy:
        condition: on-failure

Jenkins Pipeline

Using the "GitHub Organization" Jenkins job allows Jenkins to find repositories that contain Jenkinsfiles. Jenkinsfiles are the Jenkins Pipeline way to define build jobs programatically with Jenkins-flavored Groovy script. Our Jenkinsfile is where the build, test, tag, and deploy stages of the pipeline infrastructure are defined. The Jenkinsfile is then checked into the source repository. To update the pipeline, simply modify the Jenkinsfile and push changes to GitHub.

Our Jenkinsfile:

#!groovy
properties([[$class: 'BuildDiscarderProperty',
                strategy: [$class: 'LogRotator', numToKeepStr: '5']]
                ])
node {
  checkout scm
    stage('Build Images'){
      sh './build.sh'
    }
    stage('Test Flask Application'){
      try {
        sh './test.sh'
        junit 'pytestresults.xml'
        archiveArtifacts 'pytestresults.xml'
      } catch (error) {
        sh 'echo Pytests Failed'
        junit 'pytestresults.xml'
        archiveArtifacts 'pytestresults.xml'
        throw error
      }
    }
    stage('Tag and Push Images'){
        // tag and push if branch is built out of master
        if(env.BRANCH_NAME.startsWith("master")){
          sh './push.sh'
        }else{
          sh 'echo branch is not master, skipping tag and push.'
        }
      }
    stage('Deploy'){
      // deploy if branch is built out of master
      if(env.BRANCH_NAME.startsWith("master")){
        sh './deploy.sh'
      }else{
        sh 'echo branch is not master, skipping deploy.'
      }
    }
  step([$class: 'WsCleanup', cleanWhenFailure: false])
}

The Continuous Delivery stages are as follows: Build Images, Test Flask Application, Tag and Push Images, and Deploy. Jenkins adds the branch's source code to the node workspace via the ”checkout scm” command. The pipeline is activated when code is pushed to the GitHub repository.

Build Images Stage

Once triggered, Jenkins bakes the application into Docker images in the Build Images stage.

Test Flask Application Stage

Jenkins runs unit tests against the application in the Test Flask Application stage. The tests results are saved to the build as “pytestresults.xml”. If a unit test fails, the Test stage will fail. Test results are archived regardless of pass or fail. Continuous Delivery (CD) adds value to one’s software because it allows developers to address issues immediately, while the code is still fresh in their minds. Addressing issues immediately means problems get solved faster, compared with finding the issue two weeks later and having a developer retrace their steps.

Tag and Push Stage

If the branch is master, Jenkins tags the images and pushes them to Docker Trusted Registry. If it is any other branch, tagging and pushing is skipped.

Deploy Stage

The master branch should be stable if Continuous Integration and Continuous Delivery are in place. If the branch is master, one shoud deploy the application to production in Docker Datacenter. When the pipeline is complete, Jenkins cleans up the workspace via the “WsCleanup” step. Producing high-quality, stable software through Continuous Delivery means fewer problems when it comes time to deploy, allowing features to reach users faster and with fewer bugs.

Ready to see the demo? Click here.

Want to learn more about DevOps? Click here.