How to Set Up CI/CD with Jenkins and Kubernetes

How to Set Up CI/CD with Jenkins and Kubernetes

Anyone who has spent more than a few weeks running production workloads on Kubernetes eventually hits the same wall: manual deployments don’t scale, and neither does manual patience. Jenkins paired with Kubernetes solves this by giving you a build system that can scale its own workers on demand, deploy automatically, and roll back the moment something looks wrong. This guide walks through the whole setup, from the underlying architecture to a working pipeline you can copy into your own cluster today.

Why Jenkins on Kubernetes Makes Sense

Jenkins has been the default CI server for over a decade, and Kubernetes has become the default runtime for over a decade’s worth of microservices. Running Jenkins on Kubernetes (rather than next to it) means:

  • Build agents are ephemeral pods, spun up per job and destroyed afterward — no more “works on this one Jenkins agent” bugs.
  • Jenkins itself can be scaled, backed up, and rescheduled like any other workload.
  • Deployments to the cluster happen via the same API Jenkins already lives inside, simplifying RBAC and networking.

This is different from a classic Jenkins install on a static VM, where agents are long-lived machines that slowly accumulate drift.

Kubernetes Architecture Primer

Before wiring up Jenkins, it helps to be precise about what Kubernetes actually does:

  • Control plane: the API server, scheduler, controller manager, and etcd. This is the brain — it stores desired state and reconciles the cluster toward it.
  • Nodes: the workers, each running a kubelet, a container runtime (containerd in most modern clusters), and kube-proxy.
  • Pods: the smallest deployable unit — one or more containers sharing network and storage.
  • Controllers: Deployments, StatefulSets, DaemonSets — objects that manage Pods declaratively.

Jenkins on Kubernetes uses the Kubernetes plugin, which talks to the API server to create Pods as build agents on demand, then deletes them when the job finishes.

Installing Jenkins on Kubernetes

The fastest reliable path is Helm.

helm repo add jenkins https://charts.jenkins.io
helm repo update
kubectl create namespace jenkins

helm install jenkins jenkins/jenkins \
  --namespace jenkins \
  --set controller.serviceType=LoadBalancer \
  --set persistence.enabled=true \
  --set persistence.size=10Gi

Check the rollout:

kubectl -n jenkins rollout status statefulset/jenkins

Output:

Waiting for 1 pods to be ready...
statefulset rolling update complete 1 pods at revision jenkins-6d6f9c9b7f...

Retrieve the admin password and service address:

kubectl -n jenkins exec -it jenkins-0 -- \
  cat /run/secrets/additional/chart-admin-password

kubectl -n jenkins get svc jenkins

Configuring the Kubernetes Plugin

Once logged in, under Manage Jenkins → Clouds, add a Kubernetes cloud. If Jenkins is running inside the cluster, it can usually auto-detect the API server URL (https://kubernetes.default.svc) and use its own ServiceAccount token for authentication — no extra credentials needed.

A minimal Pod template YAML for a build agent:

apiVersion: v1
kind: Pod
metadata:
  labels:
    jenkins: agent
spec:
  containers:
    - name: jnlp
      image: jenkins/inbound-agent:latest
    - name: docker
      image: docker:24-cli
      command: ["cat"]
      tty: true
      volumeMounts:
        - name: docker-sock
          mountPath: /var/run/docker.sock
  volumes:
    - name: docker-sock
      hostPath:
        path: /var/run/docker.sock

RBAC for Jenkins

Jenkins needs permission to create and delete Pods in its namespace. A tightly scoped Role beats a cluster-wide binding:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: jenkins-agent-role
  namespace: jenkins
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/exec", "pods/log"]
    verbs: ["create", "delete", "get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: jenkins-agent-binding
  namespace: jenkins
subjects:
  - kind: ServiceAccount
    name: jenkins
    namespace: jenkins
roleRef:
  kind: Role
  name: jenkins-agent-role
  apiGroup: rbac.authorization.k8s.io

Apply it:

kubectl apply -f jenkins-rbac.yaml

If Jenkins needs to deploy into other namespaces, create matching Roles/RoleBindings there rather than granting a ClusterRole across the whole cluster — least privilege matters here just as much as anywhere else in the security model.

Writing the Pipeline

Here’s a Jenkinsfile that builds a Docker image, pushes it, and deploys via kubectl:

pipeline {
  agent {
    kubernetes {
      yaml """
        apiVersion: v1
        kind: Pod
        spec:
          containers:
          - name: docker
            image: docker:24-cli
            command: ['cat']
            tty: true
            volumeMounts:
            - name: docker-sock
              mountPath: /var/run/docker.sock
          - name: kubectl
            image: bitnami/kubectl:latest
            command: ['cat']
            tty: true
          volumes:
          - name: docker-sock
            hostPath:
              path: /var/run/docker.sock
      """
    }
  }
  environment {
    IMAGE = "registry.example.com/myapp:${env.BUILD_NUMBER}"
  }
  stages {
    stage('Build') {
      steps {
        container('docker') {
          sh "docker build -t $IMAGE ."
          sh "docker push $IMAGE"
        }
      }
    }
    stage('Deploy') {
      steps {
        container('kubectl') {
          sh "kubectl set image deployment/myapp myapp=$IMAGE -n production"
          sh "kubectl rollout status deployment/myapp -n production"
        }
      }
    }
  }
}

The Deployment Being Updated

For context, a typical target Deployment looks like this:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  namespace: production
spec:
  replicas: 4
  selector:
    matchLabels:
      app: myapp
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: registry.example.com/myapp:latest
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
          resources:
            requests:
              cpu: 250m
              memory: 256Mi
            limits:
              cpu: 500m
              memory: 512Mi

The RollingUpdate strategy combined with a readiness probe is what makes Jenkins-triggered deploys safe — Kubernetes won’t route traffic to a new Pod until it reports healthy, and it won’t tear down more old Pods than maxUnavailable allows.

Monitoring and Troubleshooting the Pipeline

Common failure points and how to check them:

# Agent pod stuck pending — check events
kubectl -n jenkins describe pod <agent-pod-name>

# Deployment stuck mid-rollout
kubectl -n production rollout status deployment/myapp
kubectl -n production describe deployment myapp

# Roll back a bad deploy
kubectl -n production rollout undo deployment/myapp

If agent pods stay Pending, it’s almost always resource requests exceeding node capacity, or a missing image pull secret. If a rollout hangs, check the readiness probe path is actually correct — a wrong probe path is the single most common cause of pipelines that “hang forever” on deploy.

High Availability for Jenkins Itself

Jenkins’ own controller is not naturally horizontally scalable (its state is file-based), so HA usually means:

  • Persistent volume backed by durable storage (not emptyDir).
  • Regular backups of JENKINS_HOME via a CronJob.
  • A cold-standby replica strategy rather than active-active.
apiVersion: batch/v1
kind: CronJob
metadata:
  name: jenkins-backup
  namespace: jenkins
spec:
  schedule: "0 2 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: backup
              image: bitnami/kubectl
              command: ["/bin/sh", "-c", "tar czf /backup/jenkins-$(date +%F).tar.gz /var/jenkins_home"]
              volumeMounts:
                - name: jenkins-home
                  mountPath: /var/jenkins_home
                - name: backup
                  mountPath: /backup
          restartPolicy: OnFailure
          volumes:
            - name: jenkins-home
              persistentVolumeClaim:
                claimName: jenkins
            - name: backup
              persistentVolumeClaim:
                claimName: jenkins-backup

Multibranch Pipelines for Multiple Environments

A single Jenkinsfile per repository quickly becomes limiting once a team is running feature branches, staging, and production off the same codebase. Multibranch Pipeline jobs solve this by automatically discovering branches and running the Jenkinsfile found in each one, with per-branch logic handled inside the pipeline itself:

pipeline {
  agent { kubernetes { yaml podTemplateYaml } }
  stages {
    stage('Deploy') {
      steps {
        script {
          def targetNamespace = env.BRANCH_NAME == 'main' ? 'production' : 'staging'
          container('kubectl') {
            sh "kubectl set image deployment/myapp myapp=$IMAGE -n ${targetNamespace}"
          }
        }
      }
    }
  }
}

Configuring this in Jenkins (New Item → Multibranch Pipeline) means every branch pushed to the repository gets its own build automatically, without manually creating a Jenkins job per branch — a meaningful reduction in operational overhead once a team has more than a couple of active branches at any given time.

Shared Libraries for Reusable Pipeline Logic

Once more than one or two services adopt this pattern, copy-pasting the same Jenkinsfile across repositories becomes its own maintenance burden. Jenkins Shared Libraries let common logic live in one place:

// vars/deployToKubernetes.groovy
def call(String image, String namespace) {
  container('kubectl') {
    sh "kubectl set image deployment/myapp myapp=${image} -n ${namespace}"
    sh "kubectl rollout status deployment/myapp -n ${namespace}"
  }
}

Referenced from any project’s Jenkinsfile:

@Library('shared-pipeline-library') _

pipeline {
  agent { kubernetes { yaml podTemplateYaml } }
  stages {
    stage('Deploy') {
      steps {
        deployToKubernetes("$IMAGE", "production")
      }
    }
  }
}

This is the same principle Helm charts apply to Kubernetes manifests — extract the repeated pattern once, parameterize it, and let every consuming project stay short and focused on what’s actually specific to it.

Securing Credentials in Jenkins

Credentials (registry passwords, kubeconfig files, API tokens) should never be hardcoded into a Jenkinsfile. Jenkins’ built-in credentials store, combined with the Kubernetes Credentials Provider plugin, lets pipelines reference a Kubernetes Secret directly instead:

pipeline {
  agent { kubernetes { yaml podTemplateYaml } }
  environment {
    REGISTRY_CREDS = credentials('registry-credentials')
  }
  stages {
    stage('Push') {
      steps {
        container('docker') {
          sh "docker login registry.example.com -u $REGISTRY_CREDS_USR -p $REGISTRY_CREDS_PSW"
        }
      }
    }
  }
}

Behind the scenes, this can be backed by a real Kubernetes Secret rather than credentials stored only inside Jenkins’ own database, keeping secret material consistent with how the rest of the cluster manages sensitive configuration.

Common Mistakes

  • Giving the Jenkins ServiceAccount a ClusterRole with cluster-admin “just to get it working” — this is the most common security regret teams have six months later.
  • Using latest tags in production Deployments, which defeats rollback entirely.
  • Not setting resource requests on agent Pods, causing noisy-neighbor scheduling problems.
  • Skipping readiness probes, so Jenkins reports “deployed” while the app is still crash-looping.

Summary

Jenkins on Kubernetes turns CI/CD from a set of scripts glued to a static server into a cluster-native workflow: ephemeral agents, declarative RBAC, and deploys that go through the same rolling-update machinery as any other change to the cluster. The setup cost is front-loaded — Helm install, RBAC, Pod templates — but it pays back every time a build agent disappears cleanly instead of leaking disk space for six months.

References

  • Kubernetes documentation: https://kubernetes.io/docs/home/
  • Jenkins Kubernetes plugin: https://plugins.jenkins.io/kubernetes/
  • Jenkins Helm chart: https://github.com/jenkinsci/helm-charts
  • CNCF landscape: https://landscape.cncf.io/
Total
1
Shares

Leave a Reply

Previous Post
How to Use Helm Charts in Kubernetes

How to Use Helm Charts in Kubernetes

Next Post
How to Implement Resource Quotas in Kubernetes

How to Implement Resource Quotas in Kubernetes

Related Posts