A Tutorial on setting up Jenkins in Kubernetes and minikube to run a CI/CD on a monorepo.

2021-01-19

Having several projects sharing a source code repository in a monolithic repository eases collaboration, large technical upgrades and eases continuous integration. Especially when combined with a trunk based development scheme, ensuring that all your projects are continuously integrated by your version control system. In this tutorial we explain how to get Jenkins running in minikube to achieve good performance in the CI/CD pipeline. The official Minikube documentation provided detailed instructions on how to install minikube and docker on your particular operating system. Once it's installed start the cluster:

$minikube start
                                            

Once everything is up and running we can check that it went well using the following steps

$ minikube status
minikube
type: Control Plane
host: Running
kubelet: Running
apiserver: Running
kubeconfig: Configured
timeToStop: Nonexistent
                                            
                                            

Now let's start a new monorepo using the popular version control system git.

$mkdir monorepo
$cd monorepo
$git init                                            
                                            

In true gitops fashion even the Jenkins config files will reside in the monorepo. Let's start by creating a Jenkins docker file that will create our docker image.

$mkdir kubernetes
$cd kubernetes
$editor Dockerfile
                                            

from jenkins/jenkins:jdk11
#blue ocean
RUN /usr/local/bin/install-plugins.sh blueocean
#git
RUN /usr/local/bin/install-plugins.sh git
# install Maven
USER root
RUN apt-get update && apt-get install -y maven curl apt-transport-https ca-certificates gnupg2 unzip xvfb libxi6 libgconf-2-4
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash -
RUN apt-get install -y build-essential nodejs
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -
RUN echo "deb [arch=amd64] https://download.docker.com/linux/debian stretch stable" >> /etc/apt/sources.list
RUN curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
RUN echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | tee -a /etc/apt/sources.list.d/kubernetes.list
RUN apt-get update
RUN apt-get install -y docker-ce kubectl
EXPOSE 80
                                            

In order for docker to create the docker image in the correct docker repository lets setup docker environment variables with the help of minikube.

$eval $(minikube docker-env)
                                            

Now we can build the docker image using the docker file

$docker build -t jenkinsme .
                                            

After a while the docker image is ready and uploaded to the minikube docker repository. Now we can tell kubernetes that we want a running pod through creating a file jenkins.yml in the kubernetes directory.

apiVersion: apps/v1
kind: Deployment
metadata:
    name: jenkins
spec:
    replicas: 1
    selector:
    matchLabels:
        app: jenkins
    strategy:
    type: Recreate
    template:
    metadata:
        labels:
        app: jenkins
        version: v1
    spec:
        serviceAccountName: default
        containers:
        - name: jenkins
            image: jenkinsme
            imagePullPolicy: Never
            env:
            - name: JAVA_OPTS
                value: -Djenkins.install.runSetupWizard=false
            - name: JENKINS_MASTER_POD_IP
                valueFrom:
                fieldRef:
                    fieldPath: status.podIP
            - name: JENKINS_OPTS
                value: --httpPort=80
            ports:
            - name: http-port
                containerPort: 80
            volumeMounts:
            - name: jenkins-home
                mountPath: /var/jenkins_home
            - name: docker-sock
                mountPath: /var/run/docker.sock
        volumes:
        - name: docker-sock
            hostPath:
            path: /var/run/docker.sock
        - name: jenkins-home
            emptyDir: {}
                                            
                                            

With the jenkins.yml created we can tell kubernetes to start a jenkins pod for us

$kubectl apply -f jenkins.yml
                                            

After some time you should see everything up and running

$ kubectl get pods
NAME                           READY   STATUS    RESTARTS   AGE
pod/jenkins-6654545fbd-44pfh   1/1     Running   0          5s                                            
                                            

Now we need a way to access the pod, we are going to use a service with a NodePort for this purpose.

$kubectl expose deployment jenkins --type=NodePort --name=jenkins-service
                                            

Now that the NodePort is created we can ask minikube to give us an URL

$minikube service --url jenkins-service

http://192.168.49.2:31102
                                            

Point your browser to the URL indicated

Jenkins home screen

In order to be able to add new kubernetes config files we create kustomization.yml that applies yml files that we name. Initially we only have the jenkins.yml in here.

resources:
- jenkins.yml                                            
                                            

Now lets create our first pipeline that applies any k8s changes we might need.

pipeline {
    agent any
    stages {
        stage('CD') {
            parallel {
                stage('prod k8s') {
                    when {
                        changeset "kubernetes/**"
                    }
                    steps {
                        sh 'kubectl apply -k kubernetes/'
                    }
                }
            }
        }
    }
}
                                            

Excellent, now that we have Jenkins up and running lets save progress.

$git add Jenkinsfile kubernetes
$git commit -m “added jenkins config for k8s”
                                            

Next step is to set up the Jenkins security since this is needed for Blue ocean

Jenkins security setup

Click Security Realm, Jenkins’ own user database, save Username: admin Password: youchoose Full name: your name E-mail address: your email Push Create First Admin User

Click Open Blue Ocean, Create a new Pipeline. Here you can connect to a git repository in github, bitbucket or your own server via ssh. In this tutorial we will connect via ssh to the minikube host. Find out your host ip number and enter ip number and path to the repository. Enter this in the repository URL. Add the key to your home ~/.ssh/authorized_keys and push create pipeline.

Jenkins pipeline setup

Now the pipeline runs and once it is completed Jenkins will show the pipeline. It's not that interesting since it will skip all steps due to missing (Jenkins) history. Let's fix that by adding a new java project in the monolithic repository. The spring java project provides one easy way to setup such an helloworld project.

mkdir helloworld
cd helloworld
curl https://start.spring.io/starter.zip \
    -d dependencies=web \
    -d name=helloworld \
    -d artifactId=helloworld \
    -o helloworld.zip
unzip helloworld.zip
rm helloworld.zip

In order for the java program to run in kubernetes we need to containerize it. Here we are writing a Dockerfile in the helloworld folder for this task.


FROM openjdk:11
EXPOSE 8080
ADD ./target/*.jar app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]                                            
                                            

The next step is to add the new project to the monorepo CD.

pipeline {
  agent any
  stages {
    stage('build') {
      when {
        changeset "helloworld/**"
      }
      steps {
        dir("${env.WORKSPACE}/helloworld/"){
          sh './gradlew build'
          sh 'docker build -t helloworld .'
        }
      }
    }
    stage('CD') {
        when {
          changeset "helloworld/**"
        }
        steps {
          sh 'kubectl rollout restart deploy helloworld'
        }
    }

    stage('k8s update') {
      when {
        changeset "kubernetes/**"
      }
      steps {
        sh 'kubectl apply -k kubernetes/'
      }
    }
  }
}
                                            

Then we need to tell kubernets how to run the dockerimage trough a file kubernetes/helloworld.yml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: helloworld
spec:
  replicas: 1
  selector:
    matchLabels:
      app: helloworld
  template:
    metadata:
      labels:
        app: helloworld
        version: v1
    spec:
      containers:
      - name: helloworld
        image: helloworld
        imagePullPolicy: Never
        ports:
        - containerPort: 80
        resources:
          limits:
            memory: 450Mi
                                            

We also need to give jenkins service account the access it needs to be able to update any k8s config we commit in the monorepo. In this case the adding of a new service.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-deployer
rules:
- apiGroups: ["extensions", "apps"] # "" indicates the core API group
  resources: ["pods","deployments"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: deploy-pods
subjects:
- kind: User
  name: system:serviceaccount:jenkins:default 
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role 
  name: pod-deployer
  apiGroup: rbac.authorization.k8s.io
                                            

Since jenkins cannot in the initial stage give itself access rights we'll have to do this manually in the first step.

kubectl apply -f kubernetes/rbac.yml
                                            

Now we add the new files to the kustomization.yml

resources:
- jenkins.yml
- helloworld.yml
- rbac.yml
                                            

And finish by letting git integrate the changes

git commit -am "added helloworld java project"
git push
                                            

Then go into Jenkins and push Branches > play and the pipeline will start using the new code. You can also configure the pipeline to scan the repository for changes and start automatically on changes. After a while you will see that the pipeline has executed and the helloworld pod is running in kubernetes.

Jenkins pipeline running

$ kubectl get pods
NAME                          READY   STATUS    RESTARTS   AGE
helloworld-7bf95cccfc-7r2pd   1/1     Running   0          2m57s
jenkins-fb4cc7d59-ngs64       1/1     Running   0          18m
                                            

Congratulations, you now have the skill to set up jenkins as a CI/CD pipeline in kubernetes. In such a way that Jenkins can update the cluster it is running on and can handle the pipeline for a whole monorepo.