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.
- How To Add Multiple Kubernetes Clusters to Jenkins
- How To Install Jenkins on Rocky Linux 8
- How To Install GitLab on Rocky Linux 8 With Let’s Encrypt
- Install and Configure GitLab on CentOS 8 / RHEL 8
- Install Kubernetes Cluster on Ubuntu 20.04 with kubeadm
- Install Kubernetes Cluster on CentOS 7 with kubeadm
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:
- Separate Jenkinsfiles from Sources and Prevent unwarranted editing
- Automatically clean up Jenkins Workspace after Builds Complete
- How To Install Jenkins Server on Kubernetes | OpenShift
- How To Use Multi-Branch Pipeline in Jenkins
- How To Configure Jenkins FreeIPA LDAP Authentication