DevOps, it’s a hot trend in computing, it’s the new buzz word and everyone’s talking about it. There isn’t a single agreed upon definition of DevOps but we like to think of it as the practice of IT operations and development engineers participating together through the entire service life cycle, from design and development process all the way to production support. Continuous integration and delivery or CI/CD is one of the most important part of DevOps.

Create a Continuous Integration and Continuous Delivery Pipeline

In this blog post, you’ll learn how to create a CI/CD pipeline for applications built to run on Dockers using Jenkins and Kubernetes. Our goal would be to automate the below process –

  1. Checkout code.
  2. Compile code.
  3. Run test cases.
  4. Build docker image.
  5. Push image to docker registry.
  6. Pull new images from registry.
  7. Deploy and manage images and containers.

The image below shows the CI/CD pipeline and the various tools involved. The application used as an example [but not strictly limited to] in this tutorial is a simple web service written in Java using Spring Boot framework with Maven being used as a build tool.  The various stages in the pipeline are shown in the figure below –

  1. Code changes are committed to version control system – GitHub
  2. Each commit to GitHub automatically triggers Jenkins build. Jenkins uses Maven to compile the code, run unit test and perform additional checks – code coverage, code quality etc.
  3. Once the code has been successfully compiled and all the tests have been passed. Jenkins builds a new docker image and push it to the docker registry.
  4. Jenkins notifies Kubernetes of the new image available for deployment.
  5. Kubernetes pulls the new docker image from the docker registry.
  6. Kubernetes deploys and manages the docker instance / container.

 

Tutorials

This blog post will not cover how to install and setup of Jenkins and/or Kubernetes cluster. There are numerous tutorials available on how to on different operating systems and platforms. For a quick and easy setup, this tutorial by google cloud is recommended.

If you followed the above tutorial then your setup consists of a Kubernetes master node with one or many child nodes. You’ll also see that Jenkins is setup to run inside a Kubernetes Engine or Kubernetes cluster, this reduces the compute resources needed for CI/CD.

 

Once you have successfully setup the environment, next step is to configure Jenkins to complete the setup. To do so, please follow the instructions here.

As shown in the architecture diagram above, Jenkins helps in achieving the following steps:

  1. Compile Code →                                                                                 Dockerfile
  2. Run Unit and other test cases →

 

  1. Build the docker image →
  2. Push the docker image to registry →                                              Jenkinsfile
  3. Notify Kubernetes of the new image →

Dockerfile

Instructions for Jenkins to execute for steps 1 and 2 are specified in Dockerfile. Dockerfile consists of commands to build and run the microservice.  Optionally, we can also include commands to run unit tests and perform additional checks.

Create a new file and name it Dockerfile. Place the file under the project’s root folder. A sample docker file having the instructions for building the microservice that uses maven as the build management tool is provided below.

#Docker base image : Alpine Linux with OpenJDK JRE
FROM openjdk:8-jre-alpine

#Check the java version
RUN ["java", "-version"]

#Install maven
RUN apt-get update
RUN apt-get install -y maven

#Set the working directory for RUN and ADD commands
WORKDIR /code

#Copy the SRC, LIB and pom.xml to WORKDIR
ADD pom.xml /code/pom.xml
ADD lib /code/lib
ADD src /code/src

#Build the code
RUN ["mvn", "clean"]
RUN ["mvn", "install"]

#Optional you can include commands to run test cases.

#Port the container listens on
EXPOSE 8081

#CMD to be executed when docker is run.
ENTRYPOINT ["java","-jar","target/recruitment-service-0.0.1.jar"]

Jenkinsfile

Instructions for Jenkins to execute for steps 3, 4 and 5 are specified in Jenkinsfile. A Jenkinsfile is a text file that contains the definition of a Jenkins Pipeline and is checked into source control. Create a new file and name it Jenkinsfile. Place the file under the project’s root folder. A sample Jenkinsfile which implements three-stage continuous delivery is provided below. For more information click here.

node{

  //Define all variables
  def project = 'my-project'
  def appName = 'my-first-microservice'
  def serviceName = "${appName}-backend"  
  def imageVersion = 'development'
  def namespace = 'development'
  def imageTag = "gcr.io/${project}/${appName}:${imageVersion}.${env.BUILD_NUMBER}"
  
  //Checkout Code from Git
  checkout scm
  
  //Stage 1 : Build the docker image.
  stage('Build image') {
      sh("docker build -t ${imageTag} .")
  }
  
  //Stage 2 : Push the image to docker registry
  stage('Push image to registry') {
      sh("gcloud docker -- push ${imageTag}")
  }
  
  //Stage 3 : Deploy Application
  stage('Deploy Application') {
       switch (namespace) {
              //Roll out to Dev Environment
              case "development":
                   // Create namespace if it doesn't exist
                   sh("kubectl get ns ${namespace} || kubectl create ns ${namespace}")
           //Update the imagetag to the latest version
                   sh("sed -i.bak 's#gcr.io/${project}/${appName}:${imageVersion}#${imageTag}#' ./k8s/development/*.yaml")
                   //Create or update resources
           sh("kubectl --namespace=${namespace} apply -f k8s/development/deployment.yaml")
                   sh("kubectl --namespace=${namespace} apply -f k8s/development/service.yaml")
           //Grab the external Ip address of the service
                   sh("echo http://`kubectl --namespace=${namespace} get service/${feSvcName} --output=json | jq -r '.status.loadBalancer.ingress[0].ip'` > ${feSvcName}")
                   break
           
        //Roll out to Dev Environment
              case "production":
                   // Create namespace if it doesn't exist
                   sh("kubectl get ns ${namespace} || kubectl create ns ${namespace}")
           //Update the imagetag to the latest version
                   sh("sed -i.bak 's#gcr.io/${project}/${appName}:${imageVersion}#${imageTag}#' ./k8s/production/*.yaml")
           //Create or update resources
                   sh("kubectl --namespace=${namespace} apply -f k8s/production/deployment.yaml")
                   sh("kubectl --namespace=${namespace} apply -f k8s/production/service.yaml")
           //Grab the external Ip address of the service
                   sh("echo http://`kubectl --namespace=${namespace} get service/${feSvcName} --output=json | jq -r '.status.loadBalancer.ingress[0].ip'` > ${feSvcName}")
                   break
       
              default:
                   sh("kubectl get ns ${namespace} || kubectl create ns ${namespace}")
                   sh("sed -i.bak 's#gcr.io/${project}/${appName}:${imageVersion}#${imageTag}#' ./k8s/development/*.yaml")
                   sh("kubectl --namespace=${namespace} apply -f k8s/development/deployment.yaml")
                   sh("kubectl --namespace=${namespace} apply -f k8s/development/service.yaml")
                   sh("echo http://`kubectl --namespace=${namespace} get service/${feSvcName} --output=json | jq -r '.status.loadBalancer.ingress[0].ip'` > ${feSvcName}")
                   break
  }

}

Kubernetes

Kubernetes, remember, manages containers. Kubernetes relies on a yaml file for information about the containers, replica sets etc. for deployment. This file is named deployment.yaml. The file can be under any path inside project’s root folder, just remember to update the path for deployment yaml in Jenkisfile.

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: recruitment-service-deployment
  namespace: development
  labels:
    app: recruitment-service-app
spec:
  replicas: 4
  template:
    metadata:
      labels:
        apps: recruitment-service
    spec:
      containers:
      - name: recruitment-service
        image: gcr.io/bats-qa/recruitment-service:development
        ports:
        - containerPort: 8081

If the number of replicas is more than 1 then a load balancer is required. In Kubernetes, we need to define this a Service. A sample Service yaml file for creating a load balancer is specified below. Similar to deployment.yaml file, this can be placed anywhere inside project’s root folder, but just remember to update the path for Service yaml in Jenkisfile.

apiVersion: v1
kind: Service
metadata:
  name: recruitment-as-a-service
  namespace: development
spec:
  ports:
    - name: http
      port: 8081
  type: LoadBalancer    
  selector:
    apps: recruitment-service

 

 

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.