{"id":860,"date":"2021-01-12T01:08:00","date_gmt":"2021-01-12T01:08:00","guid":{"rendered":"https:\/\/blog.ngocha.biz\/?p=860"},"modified":"2021-01-12T01:08:00","modified_gmt":"2021-01-12T01:08:00","slug":"setup-jenkins-on-kubernetes-cluster","status":"publish","type":"post","link":"https:\/\/blog.ngocha.biz\/?p=860","title":{"rendered":"How To Setup Jenkins On Kubernetes Cluster: Easy Guide"},"content":{"rendered":"<p>Hosting Jenkins on a Kubernetes cluster is beneficial for Kubernetes-based deployments and dynamic container-based scalable Jenkins agents.<\/p>\n<p>In this guide, I have explained the step-by-step process for setting up Jenkins on a <a href=\"https:\/\/devopscube.com\/setup-kubernetes-cluster-google-cloud\/\" rel=\"noreferrer noopener\">Kubernetes cluster.<\/a><\/p>\n<h2 id=\"setup-jenkins-on-kubernetes-cluster\">Setup Jenkins On Kubernetes Cluster<\/h2>\n<p>For setting up a <a href=\"https:\/\/devopscube.com\/jenkins-2-tutorials-getting-started-guide\/\" rel=\"noopener\">Jenkins<\/a> cluster on Kubernetes, we will do the following.<\/p>\n<ol>\n<li>Create a Namespace<\/li>\n<li>Create a service account with Kubernetes admin permissions.<\/li>\n<li>Create local persistent volume for persistent Jenkins data on Pod restarts.<\/li>\n<li>Create a deployment YAML and deploy it.<\/li>\n<li>Create a service YAML and deploy it.<\/li>\n<li>Access the Jenkins application on a Node Port.<\/li>\n<\/ol>\n<blockquote><p><strong>Note:<\/strong> This tutorial doesn&#8217;t use local persistent volume as this is a generic guide. For using persistent volume for your Jenkins data, you need to create volumes of relevant cloud or on-prem data center and configure it.<\/p><\/blockquote>\n<h2 id=\"jenkins-kubernetes-manifest-files\">Jenkins Kubernetes Manifest Files<\/h2>\n<p>All the Jenkins Kubernetes manifest files used in this blog are <a href=\"https:\/\/github.com\/scriptcamp\/kubernetes-jenkins?ref=devopscube.com\" rel=\"noreferrer noopener\">hosted on Github<\/a>. Please clone the repository if you have trouble copying the manifest from the blog.<\/p>\n<pre><code>git clone https:\/\/github.com\/scriptcamp\/kubernetes-jenkins<\/code><\/pre>\n<p>Use the Github files for reference and follow the steps in the next sections<\/p>\n<h2 id=\"kubernetes-jenkins-deployment\">Kubernetes Jenkins Deployment<\/h2>\n<p>Here is a high-level view of what we are going to do.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/img_0184-1.jpg\" class=\"kg-image\" alt=\"Jenkins Kubernetes architecture\" loading=\"lazy\" width=\"1548\" height=\"1260\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/03\/img_0184-1.jpg 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/03\/img_0184-1.jpg 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/img_0184-1.jpg 1548w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Let&#8217;s get started with deploying Jenkins on Kubernetes.<\/p>\n<p><strong>Step 1<\/strong>: Create a Namespace for Jenkins. It is good to categorize all the <a href=\"https:\/\/devopscube.com\/devops-tools-for-infrastructure-automation\/\" rel=\"noreferrer noopener\">devops tools<\/a> as a separate namespace from other applications.<\/p>\n<pre><code>kubectl create namespace devops-tools<\/code><\/pre>\n<p><strong>Step 2:<\/strong> Create a <code>serviceAccount.yaml<\/code> file and copy the following admin service account manifest.<\/p>\n<pre><code>---\napiVersion: rbac.authorization.k8s.io\/v1\nkind: ClusterRole\nmetadata:\n  name: jenkins-admin\nrules:\n  - apiGroups: [\"\"]\n    resources: [\"*\"]\n    verbs: [\"*\"]\n\n---\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: jenkins-admin\n  namespace: devops-tools\n\n---\napiVersion: rbac.authorization.k8s.io\/v1\nkind: ClusterRoleBinding\nmetadata:\n  name: jenkins-admin\nroleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: ClusterRole\n  name: jenkins-admin\nsubjects:\n- kind: ServiceAccount\n  name: jenkins-admin\n  namespace: devops-tools<\/code><\/pre>\n<p>The <code>serviceAccount.yaml<\/code> creates a <code>jenkins-admin<\/code> clusterRole, <code>jenkins-admin<\/code> <a href=\"https:\/\/devopscube.com\/kubernetes-api-access-service-account\/\" rel=\"noreferrer noopener\">ServiceAccount<\/a> and binds the <code>clusterRole<\/code> to the service account.<\/p>\n<p>The <code>jenkins-admin<\/code> cluster role has all the permissions to manage the cluster components. You can also restrict access by specifying individual resource actions.<\/p>\n<p>Now create the service account using kubectl.<\/p>\n<pre><code>kubectl apply -f serviceAccount.yaml<\/code><\/pre>\n<p><strong>Step 3: <\/strong>Create  <code>volume.yaml <\/code>and copy the following persistent volume manifest.<\/p>\n<pre><code>kind: StorageClass\napiVersion: storage.k8s.io\/v1\nmetadata:\n  name: local-storage\nprovisioner: kubernetes.io\/no-provisioner\nvolumeBindingMode: WaitForFirstConsumer\n\n---\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: jenkins-pv-volume\n  labels:\n    type: local\nspec:\n  storageClassName: local-storage\n  claimRef:\n    name: jenkins-pv-claim\n    namespace: devops-tools\n  capacity:\n    storage: 10Gi\n  accessModes:\n    - ReadWriteOnce\n  local:\n    path: \/mnt\n  nodeAffinity:\n    required:\n      nodeSelectorTerms:\n      - matchExpressions:\n        - key: kubernetes.io\/hostname\n          operator: In\n          values:\n          - worker-node01\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: jenkins-pv-claim\n  namespace: devops-tools\nspec:\n  storageClassName: local-storage\n  accessModes:\n    - ReadWriteOnce\n  resources:\n    requests:\n      storage: 3Gi<\/code><\/pre>\n<blockquote><p><strong>Important Note:<\/strong> Replace <code>worker-node01<\/code> with any one of your cluster worker nodes hostname.<\/p><\/blockquote>\n<p>You can get the worker node hostname using the kubectl.<\/p>\n<pre><code>kubectl get nodes<\/code><\/pre>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/nodes-1.png\" class=\"kg-image\" alt=\"\" loading=\"lazy\" width=\"601\" height=\"105\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/03\/nodes-1.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/nodes-1.png 601w\"><\/figure>\n<p>For volume, I have used the <code>local<\/code> storage class for the purpose of demonstration. Meaning, it creates a <code>PersistentVolume<\/code> volume in a specific node under <code>\/mnt <\/code>location.<\/p>\n<p>As the <code>local<\/code> storage class requires the node selector, you need to specify the worker node name correctly for the Jenkins pod to get scheduled in the specific node.<\/p>\n<p>If the pod gets deleted or restarted, the data will get persisted in the node volume. However, if the node gets deleted, you will lose all the data.<\/p>\n<p>Ideally, you should use a persistent volume using the available storage class with the cloud provider or the one provided by the cluster administrator to persist data on node failures.<\/p>\n<p>Let&#8217;s create the volume using kubectl<\/p>\n<pre><code>kubectl create -f volume.yaml<\/code><\/pre>\n<p><strong>Step 2:<\/strong> Create a Deployment file named <code>deployment.yaml<\/code> and copy the following deployment manifest.<\/p>\n<p>Here we are using the latest <a href=\"https:\/\/hub.docker.com\/r\/jenkins\/jenkins?ref=devopscube.com\" rel=\"noreferrer noopener\">Jenkins LT<\/a><a href=\"https:\/\/hub.docker.com\/r\/jenkins\/jenkins?ref=devopscube.com\" rel=\"noreferrer noopener\">S<\/a><a href=\"https:\/\/hub.docker.com\/r\/jenkins\/jenkins?ref=devopscube.com\" rel=\"noreferrer noopener\"> docker image<\/a> from the Docker hub.<\/p>\n<pre><code>apiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: jenkins\n  namespace: devops-tools\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: jenkins-server\n  template:\n    metadata:\n      labels:\n        app: jenkins-server\n    spec:\n      securityContext:\n            fsGroup: 1000 \n            runAsUser: 1000\n      serviceAccountName: jenkins-admin\n      containers:\n        - name: jenkins\n          image: jenkins\/jenkins:lts\n          resources:\n            limits:\n              memory: \"2Gi\"\n              cpu: \"1000m\"\n            requests:\n              memory: \"500Mi\"\n              cpu: \"500m\"\n          ports:\n            - name: httpport\n              containerPort: 8080\n            - name: jnlpport\n              containerPort: 50000\n          livenessProbe:\n            httpGet:\n              path: \"\/login\"\n              port: 8080\n            initialDelaySeconds: 90\n            periodSeconds: 10\n            timeoutSeconds: 5\n            failureThreshold: 5\n          readinessProbe:\n            httpGet:\n              path: \"\/login\"\n              port: 8080\n            initialDelaySeconds: 60\n            periodSeconds: 10\n            timeoutSeconds: 5\n            failureThreshold: 3\n          volumeMounts:\n            - name: jenkins-data\n              mountPath: \/var\/jenkins_home         \n      volumes:\n        - name: jenkins-data\n          persistentVolumeClaim:\n              claimName: jenkins-pv-claim<\/code><\/pre>\n<p>In this Jenkins <a href=\"https:\/\/devopscube.com\/kubernetes-deployment-tutorial\/\" rel=\"noreferrer noopener\">Kubernetes deployment<\/a> we have used the following.<\/p>\n<ol>\n<li><code>securityContext<\/code>  for Jenkins pod to be able to write to the local persistent volume.<\/li>\n<li>Liveliness and readiness probe.<\/li>\n<li>Local persistent volume based on local storage class that holds the Jenkins data path <code>\/var\/jenkins_home<\/code><\/li>\n<\/ol>\n<blockquote><p><strong>Note<\/strong>: The deployment file uses local storage class persistent volume for Jenkins data. For production use cases, you should add a cloud-specific storage class persistent volume for your Jenkins data.  See the sample implementation of <a href=\"https:\/\/devopscube.com\/persistent-volume-google-kubernetes-engine\/\" rel=\"noreferrer noopener\">persistent volume for Jenkins in Google Kubernetes Engine<\/a><\/p><\/blockquote>\n<p>If you don&#8217;t want the local storage persistent volume, you can replace the volume definition in the deployment with the host directory as shown below.<\/p>\n<pre><code>volumes:\n      - name: jenkins-data\n        emptyDir: {}<\/code><\/pre>\n<p>Create the deployment using kubectl.<\/p>\n<pre><code>kubectl apply -f deployment.yaml<\/code><\/pre>\n<p>Check the deployment status.<\/p>\n<pre><code>kubectl get deployments -n devops-tools<\/code><\/pre>\n<p>Now, you can get the deployment details using the following command.<\/p>\n<pre><code>kubectl describe deployments -n devops-tools<\/code><\/pre>\n<p>Also, You can get the details from the kubernetes dashboard as shown below.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/screen-shot-2017-10-08-at-7-05-25-pm-1.jpg\" class=\"kg-image\" alt=\"Jenkins deployment on Kubernetes cluster\" loading=\"lazy\" width=\"2000\" height=\"690\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/03\/screen-shot-2017-10-08-at-7-05-25-pm-1.jpg 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/03\/screen-shot-2017-10-08-at-7-05-25-pm-1.jpg 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1600\/2025\/03\/screen-shot-2017-10-08-at-7-05-25-pm-1.jpg 1600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w2400\/2025\/03\/screen-shot-2017-10-08-at-7-05-25-pm-1.jpg 2400w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<h2 id=\"accessing-jenkins-using-kubernetes-service\">Accessing Jenkins Using Kubernetes Service<\/h2>\n<p>We have created a deployment. However, it is not accessible to the outside world. For accessing the Jenkins deployment from the outside world, we should create a service and map it to the deployment.<\/p>\n<p><strong>Step 1:<\/strong> Create  <code>service.yaml<\/code> and copy the following service manifest.<\/p>\n<pre><code>apiVersion: v1\nkind: Service\nmetadata:\n  name: jenkins-service\n  namespace: devops-tools\n  annotations:\n      prometheus.io\/scrape: 'true'\n      prometheus.io\/path:   \/\n      prometheus.io\/port:   '8080'\nspec:\n  selector: \n    app: jenkins-server\n  type: NodePort  \n  ports:\n    - port: 8080\n      targetPort: 8080\n      nodePort: 32000<\/code><\/pre>\n<blockquote><p><strong>Note:<\/strong> Here, we are using the type as <code>NodePort<\/code> which will expose Jenkins on all kubernetes node IPs on port 32000.  If you have an <a href=\"https:\/\/devopscube.com\/setup-ingress-kubernetes-nginx-controller\/\" rel=\"noreferrer noopener\">ingress setup<\/a>, you can create an ingress rule to access Jenkins. Also, you can expose the Jenkins service as a Loadbalancer if you are running the cluster on AWS, Google, or Azure cloud.<\/p><\/blockquote>\n<p>Create the Jenkins service using kubectl.<\/p>\n<pre><code>kubectl apply -f service.yaml<\/code><\/pre>\n<p>Now if you browse to any one of the Node IPs on port <code>32000<\/code>, you will be able to access the Jenkins dashboard.<\/p>\n<pre><code>http:\/\/&lt;node-ip&gt;:32000<\/code><\/pre>\n<p>Jenkins will ask for the initial Admin password when you access the dashboard for the first time.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/jenkins-dashbaord-1.png\" class=\"kg-image\" alt=\"Jenkins Kubernetes Initial Password\" loading=\"lazy\" width=\"2000\" height=\"1032\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/03\/jenkins-dashbaord-1.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/03\/jenkins-dashbaord-1.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1600\/2025\/03\/jenkins-dashbaord-1.png 1600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/jenkins-dashbaord-1.png 2020w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>You can get that from the pod logs either from the kubernetes dashboard or  CLI. You can get the pod details using the following CLI command.<\/p>\n<pre><code>kubectl get pods -n devops-tools<\/code><\/pre>\n<p>And with the pod name, you can get the logs as shown below. replace the pod name with your pod name.<\/p>\n<pre><code>kubectl logs jenkins-deployment-2539456353-j00w5 -n devops-tools<\/code><\/pre>\n<p>The password can be found at the end of the log as shown below.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/jenkins-password-min-1.png\" class=\"kg-image\" alt=\"Jenkins Kubernetes initial admin password.\" loading=\"lazy\" width=\"824\" height=\"286\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/03\/jenkins-password-min-1.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/jenkins-password-min-1.png 824w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Alternatively, you can run the exec command to get the password directly from the location as shown below.<\/p>\n<pre><code>kubectl exec -it jenkins-559d8cd85c-cfcgk cat  \/var\/jenkins_home\/secrets\/initialAdminPassword -n devops-tools<\/code><\/pre>\n<p>Once you enter the password you can proceed to install the suggested plugin and create an admin user. All these steps are self-explanatory from the Jenkins dashboard.<\/p>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>When you host Jenkins on Kubernetes for production workloads, you need to consider setting up a highly available persistent volume to avoid data loss during pod or node deletion.<\/p>\n<p>A pod or node deletion could happen anytime in Kubernetes environments. It could be a patching activity or a downscaling activity.<\/p>\n<p>Hope this step-by-step guide helps you to learn and understand the components involved in setting up a Jenkins server on a Kubernetes cluster.<\/p>\n<p>If you are looking for setting up container-based agents on Kubernetes, check out my guide on <a href=\"https:\/\/devopscube.com\/jenkins-build-agents-kubernetes\/\" rel=\"noreferrer noopener\">setting up Kubernetes pod as Jenkins build agents<\/a>.<\/p>\n<hr>\n<p><strong>Ngu\u1ed3n:<\/strong> <a href=\"https:\/\/devopscube.com\/setup-jenkins-on-kubernetes-cluster\/\" target=\"_blank\" rel=\"noopener noreferrer\">How To Setup Jenkins On Kubernetes Cluster: Easy Guide \u2014 DevOpsCube<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Source: https:\/\/devopscube.com\/setup-jenkins-on-kubernetes-cluster\/<\/p>\n","protected":false},"author":1,"featured_media":861,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-860","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops"],"_links":{"self":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts\/860","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=860"}],"version-history":[{"count":0,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts\/860\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/media\/861"}],"wp:attachment":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=860"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=860"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=860"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}