August 1, 2019

CI/CD with Kubernetes

Kubernetes is compatible with the majority of Continuous Integration / Continuous Delivery (CI/CD) tools which allows developers to run tests, deploy builds in Kubernetes and update applications with no downtime. One of the most popular CI/CD tools now is Jenkins. This article will focus on configuring a CI/CD pipeline with Jenkins and Helm on Kubernetes.

Jenkins CI/CD Tool for the Cloud

Jenkins is a quite easy to configure, modify and extend. It deploys code instantly and generates test reports. Jenkins can be configured according to your requirements for CI/CD.

CI/CD Steps

The CI/CD process with Jenkins generally

  • checks out code,
  • runs unit tests,
  • Dockerizes an application,
  • pushes the Dockerized application to the Docker Registry, and
  • uses Ansible Playbooks to deploy the Dockerized application on K8s.

To see how it works, let’s start with the installation for Jenkins. We will use a CentOS 7 machine with Docker and Kubernetes already installed.

Installing Jenkins

Step one: update your CentOS 7 system.

sudo yum install epel-release nodejs
sudo yum update

Step two: install Java.

sudo yum install java-1.8.0-openjdk.x86_64
sudo cp /etc/profile /etc/profile_backup
echo 'export JAVA_HOME=/usr/lib/jvm/jre-1.8.0-openjdk' | sudo tee -a /etc/profile 
echo 'export JRE_HOME=/usr/lib/jvm/jre' | sudo tee -a /etc/profile source /etc/profile

Step three: install Jenkins

sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key
wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
sudo yum install -y jenkins

Step four: start Jenkins and check if it is running.

sudo systemctl start jenkins
sudo systemctl status jenkins

Step five: set up Jenkins.

To start setting up Jenkins, we need to visit its web dashboard running on port 8080. Open your browser and see your public IP address or your domain name followed by the port number.

http://YOUR_IP_OR_DOMAIN:8080

You will see a page like the one below.

To obtain the initial admin password, run:

sudo cat /var/lib/jenkins/secrets/initialAdminPassword

Paste the password in the “Administrator password” field and hit continue. If you are new to Jenkins, we recommend you select “Install suggested plugins”. Now you can see that Jenkins is installing some plugins.

After that, you will be redirected to a page where you have to create your first admin user.

Preparing the Jenkins Server

Jenkins offers a simple way to set up a CI/CD environment for almost any combination of languages and source code repositories. Let’s configure Jenkins Server, which involves Docker, Ansible, Helm, and Docker plugins.

Configuring Docker

Docker is hotter than hot because it makes it possible to get far more apps running on the same old servers and it also makes it very easy to package and ship programs. Let’s create jenkins user for the docker group.

$ sudo groupadd docker
$ sudo usermod -aG docker jenkins
$ chmod root:docker /var/run/docker.sock

Go to /etc/passwd.

Find

jenkins:x:996:993:Jenkins Automation Server:/var/lib/jenkins:/bin/false

and change it to

jenkins:x:996:993:Jenkins Automation Server:/var/lib/jenkins:/bin/bash.

Add the Ansible host to /etc/ansible/hosts.

[localhost]
127.0.0.1

Make sure Jenkins can access the Kubernetes cluster using kubectl.

mv .kube/config to /var/lib/jenkins

Finally, add jenkins to sudo users.

$ visudo -f /etc/sudoers
jenkins ALL=NOPASSWD: ALL

Ansible is an open source automation platform. It is very, very simple to setup and yet powerful. Ansible can help you with configuration management, application deployment, and task automation. It can also do IT orchestration, where you can run tasks in sequence and create a chain of events to run on several different servers or devices.

$ sudo yum install python-pip
$ sudo pip install ansible

Installing and Configuring the Helm Package Manager

Helm helps you manage Kubernetes applications. With Helm Charts you can define, install, and upgrade even the most complex Kubernetes applications.

$ wget https://storage.googleapis.com/kubernetes-helm//var/lib/jenkins/ansible/demo-deploy/deploy.yml-v2.8.1-linux-amd64.tar.gz
$ tar -xzvf helm-v2.8.1-linux-amd64.tar.gz
$ sudo mv linux-amd64/helm /usr/local/bin
$ sudo -i -u jenkins
$ mkdir .kube ; $ touch .kube/config

Copy the contents of /etc/kubernetes/admin.conf to ~/.kube/config. In doing this, you can access the Kubernetes cluster under user jenkins, if necessary. Now let’s initialise Helm.

$ helm init --upgrade

Installing the Jenkins Docker Plugin

The Docker plugin allows us to use Docker to dynamically provision build agents, run a single build, and then push an image to the registry. Navigate to http://your-ip:8080/pluginManager/available and search for the plug-in “CloudBees Docker Build and Publish”. Click “Download Now” and check the box to restart.

Creating the Jenkins Pipeline

Go to Jenkins and select “New Item” on the left side, enter the name “POC”, select Pipeline then click OK.

Generating Pipeline Syntax for Git and the Docker Registry

The Pipeline Syntax section (/job/PIPELINE/pipeline-syntax/) will help you generate the Pipeline Script code which can be used to define various steps. Pick a step you are interested in, configure it, click Generate Pipeline Script, and you will see a Pipeline Script statement that would call a step with that configuration.

Select git and provide the repository URL and user-name/password. If the repository is private, it will generate the syntax for you. If you use Docker Hub, select withdocker-registry which we installed before and provide the credentials of the registry (https://index.docker.io/v1/ for Docker Hub) . Click “Generate Pipeline Script” and you will get a script like that that you will use as credentials.

withDockerRegistry([credentialsId: '85f99fe6-cff4–9064-a85s-a77de72ad87h', url: 'https://index.docker.io/v1/'])

Cloning a Helm chart

Let’s clone a chart for our sample project.

$ sudo su - jenkins 
$ mkdir ansible
$ git clone https:URL
$ cp -r CI/CD-K8s/ansible/Demo ansible/

This will clone a sample project with a hello-world type of application. The Helm charts for our project are located at ansible/Demo/templates. You can replace the YAML with your own files for deployment and services. Here’s the deployment.yml we will use:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: welcome to kubernetes
spec:
  replicas: 3
  selector:
    matchLabels:
      app:welcome to kubernetes
  template:
    metadata:
      labels:
        app: welcome to kubernetes
    spec:
      containers:
      - name: welcome to kubernetes
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        ports:
        - containerPort: 5000
        imagePullPolicy: Always

And this is the service.yml:

apiVersion: v1
kind: Service
metadata:
  name: welcome to kubernetes
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: welcome to kubernetes

Configure Ansible to Deploy the Helm Chart

Let’s create an Ansible playbook to call the helm chart.

$ cp -r CI/CD-K8s/ansible/Demo-deploy /var/lib/jenkins/

The playbook we will use in this article looks like this:

- hosts: localhost
  vars:
    ImageName: ""
    Namespace: ""
    imageTag: ""
  #remote_user: ansible
  #become: true
  gather_facts: no
  connection: local
  tasks:
    - name: Create Namespace {{ Namespace }}
      command: "kubectl create namespace {{ Namespace }}"
      ignore_errors: yes
    - name: Deploy Demo
      command: "/usr/local/bin/helm install --name=Demo-{{ Namespace }}  --namespace={{ Namespace }} ../Demo --set image.repository={{ ImageName }} --set image.tag={{ imageTag }} --set namespace={{ Namespace }}"
      delegate_to: localhost
      ignore_errors: yes
    - name: Update K8s App
      command: "/usr/local/bin/helm upgrade --wait --recreate-pods --namespace={{ Namespace }} --set image.repository={{ ImageName }} --set image.tag={{ imageTag }} --set namespace={{ Namespace }} Demo-{{ Namespace }} ../K8s"
      delegate_to: localhost
      ignore_errors: yes

CI/CD with Jenkins Pipeline

The Jenkinsfile that we will use for the pipeline looks like this:

node{
  def Namespace = "default"
  def ImageName = "Demo/K8s"
  def Creds = "3dhf6hhd-a300-78ee-kjg5-7j3dfhjg7764"
  try{
  stage('Checkout'){
      git 'CI/CD-K8s.git
      sh "git rev-parse --short HEAD > .git/commit-id"
      imageTag= readFile('.git/commit-id').trim()
}
stage('RUN Unit Tests'){
      sh "npm install"
      sh "npm test"
  }
  stage('Docker Build, Push'){
    withDockerRegistry([credentialsId: "${Creds}", url: 'https://index.docker.io/v1/']) {
      sh "docker build -t ${ImageName}:${imageTag} ."
      sh "docker push ${ImageName}"
        }
}
    stage('Deploy on K8s'){
sh "ansible-playbook /var/lib/jenkins/ansible/Demo-deploy/deploy.yml  --user=jenkins --extra-vars ImageName=${ImageName} --extra-vars imageTag=${imageTag} --extra-vars Namespace=${Namespace}"
    }
     } catch (err) {
      currentBuild.result = 'FAILURE'
    }
}

Let’s look deeper into the Jenkinsfile. Step one: defining variables.

def Namespace = "default"  
//default namespace on k8s
def ImageName = "Demo/K8s" 
// image name which will be pushed to docker registry
    def Creds = "2dfd9d0d-a300-49ee-vhaf-0a5fgcaa5279" 
// Creds of docker registry

Step two: pull/clone updates from our version control.

git 'URL'
      sh "git rev-parse --short HEAD > .git/commit-id"
      imageTag= readFile('.git/commit-id').trim()

Step three: run unit tests.

stage('RUN Unit Tests'){
         sh "npm install"
         sh "npm test"
    }

Step four: Docker build and push to the Docker Registry.

stage('Docker Build, Push'){
withDockerRegistry([credentialsId: "${Creds}", url: 'https://index.docker.io/v1/']) {
         sh "docker build -t ${ImageName}:${imageTag} ."
         sh "docker push ${ImageName}"
           }
      }

Step five: call the Ansible playbook to deploy on K8s.

stage('Deploy on K8s'){
sh "ansible-playbook ./ansible/demo-deploy/deploy.yml  --user=jenkins --extra-vars ImageName=${ImageName} --extra-vars imageTag=${imageTag} --extra-vars Namespace=${Namespace}"
 }

Access the application running in Kubernetes.

$ kubectl get svc // to get the IP/Port of the application

Now cURL http://public-node-ip:node-port.

Update the Code

Now let’s see if we got it right. Let’s change our YAML files a little. In CI/CD-K8s/app/routes/root.js change “welcome to K8s” to “update k8s” in line 3.

module.exports = function(req, res, next) {
  res.contentType = "json";
  res.send({ message: "welcome to  K8s" });
  next();
};

In CI/CD-K8s/app/test/root.test.js change “Thanks for learning K8s to “update k8s” in line 27.

const chai = require("chai");
const sinon = require("sinon");
var rootResponder = require("../routes/root");
// const expect = chai.expect;
// const assert = chai.assert;
chai.should();
describe("Root Directory Test", function() {
  describe("Should Behave properly on GETing /", function() {
    const nextSpy = sinon.spy();
    const resSpy = { send: sinon.spy() };
    beforeEach(function() {
      nextSpy.resetHistory();
      resSpy.send.resetHistory();
    });
    it("should call next", function() {
      rootResponder({}, resSpy, nextSpy);
      nextSpy.calledOnce.should.be.true;
    });
it("should call send on resp", function() {
      rootResponder({}, resSpy, nextSpy);
      resSpy.send.calledOnce.should.be.true;
    });
    it("should call send on resp with Hello World as a message", function() {
      rootResponder({}, resSpy, nextSpy);
      resSpy.send.calledWith({ message: "Thanks for learning K8s" }).should.be.true;
    });
    it("should have json as the content type of the respones", function() {
      rootResponder({}, resSpy, nextSpy);
      resSpy.contentType.should.exist;
      resSpy.contentType.should.equal("json");
    });
  });
});

Run the pipeline again and cURL http://public-node-ip:node-port. There we have it – we’ve demonstrated a simple CI/CD workflow with Jenkins, Docker, Ansible, Helm and Kubernetes!

By the way, check out our best AWS deal: https://www.avmconsulting.net/well-architected-review