Seamless CI/CD with Jenkins + Helm and Kubernetes
Tags: kubernetes, devops
Published: 2019-10-02 15:20:41

There are many ways and platforms you can use to solve [CI](https://en.wikipedia.org/wiki/Continuous_integration)/[CD](https://en.wikipedia.org/wiki/Continuous_delivery) for your company nowadays. As we were already using Jenkins jobs for deployments and everyone at the team knew it well it was the obvious choice, but we shifted towards pipelines.

To implement this you need:

- A [Kubernetes](https://kubernetes.io/) cluster where you are going to deploy your applications
- [Helm](https://helm.sh/) deployment files for your application
- [Jenkins](https://jenkins.io/doc/book/pipeline/) CI pipelines

# The big picture
Jenkins will fetch branches and tags from the selected repository and run all steps defined for the pipeline inside your project's `Jenskinsfile`.

Kubernetes is an orchestration tool for containers so we need to be deploying applications as Docker containers so they can run and be managed inside this orchestration tool.

Helm is a “package manager” for Kubernetes. It reads, parses and injects values into a series of “Helm templates” that in the results in a series of Kubernetes objects. Helm then bundles all of these objects into a Helm release and installs it in the configured Kubernetes cluster. You can supply to helm some input values to configure each installation which is flexible enough to install for example the same application configured differently for multiple environments (e.g dev, staging, and production).

# The jenkins pipeline
Here is an example of our declarative Jenkins pipeline file:

```groovy
pipeline {
    agent {
        kubernetes {
            label 'my-app'
            yamlFile 'JenkinsKubernetesPod.yaml'
        }
    }
    stages {
        stage('Run unit tests') {
            steps {
              // The needed steps for your testing
            }
        }

        stage('Build application') {
            steps {
              // Build the app
            }
        }
      
        stage('Docker publish') {
            steps {
              // Publish a docker image for your application 
            }
        }

        stage('Deployment') {
            steps {
                script {
                  container('helm') {
                      // Init authentication and config for your kubernetes cluster
                      sh("helm init --client-only --skip-refresh")
                      sh("helm upgrade --install --wait prod-my-app ./helm --namespace prod")
                    }
                }
            }
        }
    }
}
```

You should include a file with the name “Jenkinsfile” in your project repository and Jenkins will use it to define the pipeline. For brevity, I only included the deployment step. In this step, you will call the Helm installation or upgrade. This will apply a `helm upgrade` to your previously installed Helm release or if no release exists Helm will execute a `helm install` with the given release name (e.g. prod-my-app).

It’s a good idea to include the namespace as prefix or suffix for the helm release name because release names are only a name for helm and not installed as part of a Kubernetes namespace.

If you attempt to install a `release-dummy` on namespace `dev` and a `release-dummy` on namespace `staging` it will fail because `release-dummy` will already exist and Helm doesn’t care about namespace. To solve this we include a prefix of the namespace on the release name.

# The deployment file
Make sure you have configured your app to run as a **Kubernetes Deployment** or **StatefulSet** or avoid having downtime between deploys. Check the `deployment.yaml` helm template below:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-dp
spec:
  replicas: 2
  minReadySeconds: 60
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  selector:
    matchLabels:
      app: {{ .Values.app.name }}
  template:
    metadata:
      labels:
        app: {{ .Values.app.name }}
    spec:
      containers:
      - name: {{ .Values.app.name }}
        image: {{ .Values.app.image.repository }}/{{ .Values.app.image.name }}:{{ .Values.app.image.tag }}
        imagePullPolicy: Always
        envFrom:
        - configMapRef:
            name: config-{{ .Values.app.name }}
        ports:
        - name: http-{{ .Values.app.name }}
          protocol: TCP
          containerPort: 80
```

And that’s it, there are a lot of concepts to learn to put this all together but when you have it all covered up you will never have to put that much effort into deploying new applications and managing releases.

When you are done you push the commits into master or create a new tag and the code will appear in your servers without no more clicking.