In case you would wish to separate the important Jenkinsfile that powers your pipeline as well as Kubernetes YAML files that determine how your application will be deployed in your clusters, then this guide might point you in the direction you would wish to go. We are assuming your pipelines end up in Kubernetes as containers and that you use Jenkins as the CI/CD engine. End result is that the developers will concentrate on code while their applications get deployed by Jenkins using files located in a different repository(s). Let us break it down and see how this can be done.

Pre-requisites

  • A working Kubernetes cluster
  • A working Jenkins instance
  • A git repository (GitLab, Github or the one you prefer). We will use Gitlab for this example.
  • Jenkins integrated to Kubernetes

In case one of the above is missing, the guides below will do the tricks.

Step 1: Create a separate repository for your Jenkinsfile and manifests

This one will depend on you. If you would wish the two files to reside in different repositories or in one, that is a decision you will make. You can follow this guide to see how to configure your Jenkins to use a remote Jenkinsfile away from developer’s code. In the end, it depends on how you would like to organize your files.

Step 2: Create deployment template

If you are using one standard language in your environment, we recommend creating one template that will serve all of your applications. Again, this is a bias and people will choose different ways of doing this which is well, very okay. For this example, we will create a template that captures most the things that we saw as sufficient. The deployment file is as follows:

---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: APP
  name: DEPLOYMENTNAME
  namespace: NAMESPACE
spec:
  selector:
    matchLabels:
      app: APP
  replicas: 1
  minReadySeconds: 15
  template:
    metadata:
      labels:
        app: APP
    spec:
      volumes:
        - name: timezone-configuration
          hostPath:
            path: /usr/share/zoneinfo/Africa/Nairobi
      containers:
        - name: APP
          image: ImageName
          ports:
            - containerPort: PORT
          imagePullPolicy: IfNotPresent
          env:
           - name: SPRING_PROFILES_ACTIVE
             value: SPRINGPROFILE
           - name: TZ
             value: Africa/Nairobi          
          volumeMounts:
          - name: timezone-configuration
            mountPath: /etc/localtime
      dnsPolicy: ClusterFirst
      dnsConfig:
        nameservers:
          - 192.168.3.215
          - 8.8.8.8            
---
apiVersion: v1
kind: Service
metadata:
  name: SERVICENAME
  namespace: NAMESPACE
  labels:
    run: SERVICENAME
spec:
  type: NodePort
  ports:
    - name: PORTNAME
      port: PORT
      protocol: TCP
  selector:
    app: APP

As you can see from the manifest, I have a deployment and its corresponding service. Another thing you will notice are the names in capital. I use them as placeholders which will be replaced by actual configurations in the Jenkinsfile as you will see in a minute. With this, I can re-use this template to power many other applications that conform to it. Again, if there is a better way this can be done, it will be awesome if that is shared.

Step 2: Create your Jenkisfile

We will use the declarative structure of the file and a sample is shared below

pipeline {
    agent any
    environment {
        VERSION = version()
        workspace = pwd()
        serviceName = name()
        registryCredential = "CredsHarb"
        ImageName = "registry.computingforgeeks.com/geekservice/${serviceName}:${VERSION}.${BUILD_NUMBER}"
        deploymentName = "geekservice-deployment"
        appName = "geekservice"
        deployServiceName = "geeks-service"
        servicePortName = "geekservice-port"
        servicePort = "8000"
    }

// This stage will clone the repository configured in Jenkins containing the dev's sources

    stages {
        stage ('Clone Repository'){
            steps {
                checkout scm
            }
        }

// This stage will create a directory called Deployment within the current workspace
// Then clone the repository files that has the deployment template into it

        stage('Get Deployment YAML') {
            steps {
                sh 'mkdir -p Deployment'
                dir("Deployment")
                    {
                    git branch: "main",
                    credentialsId: 'JenkinsUser',
                    url: '//gitlab.computingforgeeks.com/beta/deploymentfiles.git'
            }
        }
    }  

// This stage will build and package your Java into a jar using Maven installed with name M2

        stage ('Build Jar File') {
            steps {
                withMaven(maven: 'M2') {
                withSonarQubeEnv(installationName: 'sonarqube-server', credentialsId: 'sonarqubeSecret') {
                sh 'mvn clean package sonar:sonar -Dsonar.projectVersion=${BUILD_NUMBER}'
                }
            }
            }
        }

        stage('Build Docker Image'){
            steps {
               script{
                app = docker.build("${ImageName}")
                }
             }
        }

        stage('Push Image to Docker Registry') {
          steps{
            script {
              docker.withRegistry("https://registry.computingforgeeks.com","CredsHarb"){
                appname = app.push("${VERSION}.${BUILD_NUMBER}")
              }
            }
          }
        }


// This dev stage has the script tag that will scope variables specific to the branch
// Such as srping profile and kubernetes namespace
// This is because each branch has different configurations
// Using sed, the values in capital will be replaced with the ones we want within Deployment/template.yaml file.
// Then later deploy to kubernetes cluster

        stage('Deploy for develop branch') {
            when {
                branch 'develop'
            }          
        steps {

                script {
                    def clusterNamespace = 'geeksapp'
                    def springProfile = 'dev'
                    def kubeOptions = [clusterName: 'kubernetes', credentialsId: 'KubeSecret', serverUrl: 'https://192.168.3.54:6443']
                    withKubeCredentials(kubectlCredentials: [kubeOptions]){
                        echo "Deploying yaml to ${clusterNamespace}"
                        sh "sed -i 's|ImageName|${ImageName}|' Deployment/template.yaml"
            sh """sed -i "s|NAMESPACE|${clusterNamespace}|" Deployment/template.yaml"""
                        sh """sed -i "s|APP|${appName}|" Deployment/test.yaml"""
                        sh """sed -i "s|SERVICENAME|${deployServiceName}|" Deployment/template.yaml"""
                        sh """sed -i "s|PORTNAME|${servicePortName}|" Deployment/template.yaml"""
                        sh """sed -i "s|PORT|${servicePort}|" Deployment/template.yaml"""
                        sh """sed -i "s|SPRINGPROFILE|${springProfile}|" Deployment/template.yaml"""
                        sh """sed -i "s|DEPLOYMENTNAME|${deploymentName}|" Deployment/template.yaml"""
                        sh "kubectl apply -f Deployment/template.yaml"
                        sh "docker rmi ${ImageName}"
                    }
                }                
            }
        }
    }
}

def version(){
    pom = readMavenPom file: 'pom.xml'
    return pom.version
}

def name(){
    pom = readMavenPom file: 'pom.xml'
    return pom.name
    }

As it can be clearly seen, the capital letters that we used in the deployment file are clearly very useful here. They are being used to replace them with the real values that we want. So this Jenkinsfile becomes highly configurable because any user can just edit the values of the variables declared in the environment {} section for global-wide values and the ones scoped withing each branch under script {}. The values entered will then appear in your deployment file and your application will be successfully deployed to the requisite Kubernetes cluster. In case you have multiple clusters, you can check out this guide on how you can connect and deploy to any of them via Jenkins. The final auto-populated deployment file for “developer” branch will look like the one below with the values entered in Jenkinsfile appearing clearly.

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: geekservice
  name: geekservice-deployment
  namespace: geeksapp
spec:
  selector:
    matchLabels:
      app: geekservice
  replicas: 1
  minReadySeconds: 15
  template:
    metadata:
      labels:
        app: geekservice
    spec:
      volumes:
        - name: timezone-configuration
          hostPath:
            path: /usr/share/zoneinfo/Africa/Nairobi
      containers:
        - name: geekservice
          image: registry.computingforgeeks.com/geekservice/geekservice-service:0.0.1.1
          ports:
            - containerPort: 8000
          imagePullPolicy: IfNotPresent
          env:
           - name: SPRING_PROFILES_ACTIVE
             value: dev
           - name: TZ
             value: Africa/Nairobi          
          volumeMounts:
          - name: timezone-configuration
            mountPath: /etc/localtime
      dnsPolicy: ClusterFirst
      dnsConfig:
        nameservers:
          - 192.168.3.215
          - 8.8.8.8            
---
apiVersion: v1
kind: Service
metadata:
  name: geeks-service
  namespace: geeksapp
  labels:
    run: geeks-service
spec:
  type: NodePort
  ports:
    - name: geekservice-port
      port: 8000
      protocol: TCP
  selector:
    app: geekservice

And we are done.

Books For Learning Kubernetes Administration:

Conclusion

Now we have successfully separated the deployment manifests and Jenkinsfile from developer’s sources and they can be fine tuned to befit your needs. Now the developers can focus on programming without worrying about the deployment.

We hope it was useful in your use-case and other ways of doing it are welcome. Thank you for following through to the end and we appreciate the feedback and awesome support. Be blessed guys.

Other guides you might find interesting include:

LEAVE A REPLY

Please enter your comment!
Please enter your name here