Intermediate

Running Jenkins in Kubernetes

This course, led by Wes Higbee, is called Running Jenkins in Kubernetes. It is aimed at engineers and DevOps who want to go beyond the traditional constraints of managing Jenkins agents.

Table of Contents

  1. Course presentation
  2. Deploy Jenkins with a Manifest
  1. Dynamic and Scalable Kubernetes Agents
  1. Using Pods in Pipelines
  1. Using JCasC with the Chart Helm
  1. Course Resources and Repositories

1. Course presentation

Hello everyone! This course, led by Wes Higbee, is called Running Jenkins in Kubernetes. It is aimed at engineers and DevOps who want to go beyond the traditional constraints of managing Jenkins agents.

Problem

Traditionally, provisioning Jenkins agents is a tedious task:

  • A matrix of different software and versions must be installed on each agent.
  • Demand must be anticipated in advance to have enough capacity.
  • Maintenance quickly becomes cumbersome over time.

Proposed solution

This course shows a modern alternative: using ephemeral agent pods created on demand, directly in Kubernetes. Each build triggers the creation of a pod, which is deleted at the end of the pipeline. No need to pre-provision anything.

Main topics covered

  1. Deploy Jenkins on a Kubernetes cluster.
  2. Run agent pods on demand, without pre-provisioning.
  3. Customize these pods according to the specific needs of each pipeline (tools, versions, Docker images).
  4. Use the official Helm chart for a one-click, fully configured Jenkins deployment.

Prerequisites

Before starting this course, it is recommended to be familiar with:

  • The basics of a Jenkins pipeline (declarative or scripted).
  • Fundamental resources of Kubernetes (pods, services, namespaces, volumes).

2. Deploy Jenkins with a Manifest

In this module, we deploy Jenkins manually on a local Kubernetes cluster using YAML manifest files. We cover the entire chain: namespace, StatefulSet, PersistentVolume, StorageClass and Services.


2.1 Starting minikube

The first step is to create a local Kubernetes cluster with minikube. The course offers scripts for Windows and macOS (available in the cluster/ directory of the course repository).

The comments at the top of the script indicate how to install the necessary tools (minikube, Helm, Docker) via Homebrew.

The script performs two essential operations:

  1. Start minikube with a profile named manifest (this name corresponds to the first cluster of the course, used for deployment by manifest).
  2. Select this profile as the active profile.
# Démarrer minikube avec un nœud et le profil "manifest"
minikube start --nodes=1 --profile=manifest

# Sélectionner ce profil comme contexte actif
minikube profile manifest

Once the cluster has started, you can list the profiles and check the status:

# Lister les profils / clusters minikube
minikube profile list

# Lister les contextes Kubernetes
kubectl config get-contexts

# Vérifier les ressources du namespace par défaut
kubectl get all

# Lister les nœuds du cluster
kubectl get nodes

We see that there is a single node, that the context is indeed manifest, and that the Kubernetes service is present in the default namespace.


2.2 Creating the Jenkins Namespace

The first thing to do is to partition part of the cluster to install Jenkins there. By default, a new minikube cluster only has the built-in namespaces (default, kube-system, etc.).

The goal is to create a reproducible deployment of Jenkins. Rather than using kubectl create namespace jenkins, we create a declarative manifest file.

File manifest/ns.yaml:

apiVersion: v1
kind: Namespace
metadata:
  name: jenkins

Application commands:

# Vérifier les namespaces existants
kubectl get namespaces

# Appliquer le fichier : crée le namespace s'il n'existe pas
kubectl apply -f ns.yaml

# Vérifier que le namespace jenkins a bien été créé
kubectl get namespaces

# Afficher les contextes Kubernetes actuels
kubectl config get-contexts

# Définir le namespace jenkins comme namespace par défaut du contexte courant
kubectl config set-context --current --namespace=jenkins

# Vérifier que le namespace par défaut est bien jenkins
kubectl config get-contexts

# Toutes les commandes kubectl suivantes cibleront le namespace jenkins
kubectl get all

The benefit of setting the context’s default namespace is that all subsequent kubectl commands automatically apply to the jenkins namespace, without having to add -n jenkins each time.


2.3 Jenkins via a StatefulSet

Once the namespace has been created, you must start Jenkins in a container. This container will be based on the official Jenkins image and will run in a Kubernetes pod.

Why a StatefulSet and not a Deployment?

Although a Deployment may be suitable, a StatefulSet is a better choice for Jenkins for the following reasons:

  • Jenkins is a *stateful application: it needs to persist its jenkins_home directory.
  • A StatefulSet guarantees that the pod always has the same name (e.g. jenkins-0), which allows it to be associated in a stable and deterministic way with its PersistentVolumeClaim (PVC).
  • If the pod fails or is deleted, the StatefulSet immediately recreates it with the same name and volume.

File manifest/jenkins-sts.yaml:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: jenkins
  namespace: jenkins
spec:
  selector:
    matchLabels:
      app: jenkins
  replicas: 1
  serviceName: jenkins
  template:
    metadata:
      labels:
        app: jenkins
    spec:
      containers:
        - name: jenkins
          image: jenkins/jenkins:lts
          ports:
            - containerPort: 8080
              name: http
            - containerPort: 50000
              name: agent
          volumeMounts:
            - name: jenkins-home
              mountPath: /var/jenkins_home
  volumeClaimTemplates:
    - metadata:
        name: jenkins-home
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 1Gi

Note: The volumeClaimTemplates section automatically creates a PersistentVolumeClaim (PVC) named jenkins-home-jenkins-0. This PVC represents the demand for storage. The Kubernetes StorageProvisioner will take care of creating the corresponding physical volume.


2.4 Start and connect to Jenkins

Before applying the StatefulSet, you can monitor the creation of resources in real time:

# Surveiller les ressources en temps réel
watch kubectl get all

# Dans un autre terminal, appliquer le StatefulSet
kubectl apply -f jenkins-sts.yaml

We observe the progression:

  1. The StatefulSet is created.
  2. A pod jenkins-0 appears with status ContainerCreating (the image is being downloaded).
  3. After a few moments, the status changes to Running.

To access the Jenkins web interface without configuring a Service, we use a direct port-forward to the pod:

# Redirection de port : accès local au pod jenkins-0 sur le port 8080
kubectl port-forward pod/jenkins-0 8080:8080

We can then open http://localhost:8080 in a browser. Jenkins asks for an initial administrator password for unlocking.

To find this password in the pod logs:

# Afficher les logs du pod jenkins-0
kubectl logs pod/jenkins-0

Initial password appears in log output. Jenkins also says it is stored in the /var/jenkins_home/secrets/initialAdminPassword file.


2.5 Provisioning the PersistentVolume for /var/jenkins_home

The volumeClaimTemplates section of the StatefulSet creates a PVC (PersistentVolumeClaim). Here is the complete storage chain in Kubernetes:

StatefulSet
    └── crée le Pod
    └── crée le PVC (PersistentVolumeClaim) ← demande de stockage
              ↓
         StorageProvisioner (provisioner automatique)
              ↓
         crée le PV (PersistentVolume) ← allocation réelle du stockage
              ↓
         (basé sur la StorageClass → définit les paramètres de création)

In minikube, the default provisioner is hostpath: it creates a directory on the host node. This means that Jenkins data is physically stored on the minikube node.

To see all these elements at once:

# Lister PV, PVC et StorageClass simultanément
kubectl get pv,pvc,sc

To see where exactly the files are located:

# Décrire le PersistentVolume pour connaître son chemin sur le nœud
kubectl describe pv <nom-du-pv>

The output indicates a path (path on the node) and a type: HostPath.

To access the minikube node and inspect files directly:

# Se connecter en SSH au nœud minikube
minikube ssh

# Dans le shell du nœud, changer vers le chemin du PV
cd /tmp/hostpath-provisioner/jenkins/jenkins-home-jenkins-0/

# Lister les fichiers Jenkins
ls

We find the complete structure of the jenkins_home directory persisted on the node.


2.6 Initial AdminPassword via HostPath directory

Once in the directory on the node (via minikube ssh), we can find the initial password in the same way as Jenkins reports it in its logs:

# Dans le shell minikube SSH
cd /tmp/hostpath-provisioner/jenkins/jenkins-home-jenkins-0/

# Naviguer vers les secrets
cd secrets

# Lire le mot de passe administrateur initial
cat initialAdminPassword

The password displayed is identical to the one that appears in the logs of pod jenkins-0. This is a good way to understand that logs and the filesystem share the same information.


2.7 What happens if the Pod fails?

One of the main advantages of StatefulSet is its ability to automatically recreate the pod in case of failure.

To demonstrate this, we monitor several resources simultaneously:

# Surveiller l'état de toutes les ressources en temps réel
watch kubectl get pv,pvc,sc,statefulset,pod

In another terminal, we simulate a pod failure by deleting it:

# Supprimer le pod pour simuler une panne
kubectl delete pod jenkins-0

We immediately observe that:

  1. Pod jenkins-0 enters Terminating state.
  2. It is immediately recreated with the same name jenkins-0.
  3. The StatefulSet ensures that the pod always has the same name, allowing it to attach to exactly the same PVC as before — and therefore to the same storage.

What happens if we delete the StatefulSet?

# Supprimer le StatefulSet en référençant le fichier
kubectl delete -f jenkins-sts.yaml

We observe:

  • StatefulSet disappears.
  • Pod jenkins-0 also disappears.
  • But the PVC and PV remain intact — the StatefulSet does not delete the PVC it ​​created, by default.

If we reapply the StatefulSet:

kubectl apply -f jenkins-sts.yaml

The pod is recreated, finds the same PVC, and no data was lost. The initial password is always the same.


2.8 The default Reclaim Policy: Delete

The next question is: what happens if we remove the PVC itself?

First of all, you must delete the StatefulSet (which owns the PVC):

# Supprimer le StatefulSet
kubectl delete -f jenkins-sts.yaml

# Supprimer le PVC
kubectl delete pvc jenkins-home-jenkins-0

We observe that:

  • PVC disappears.
  • The PV also disappears — and with it, all the Jenkins files.

This happens because of the Reclaim Policy. By default, minikube’s StorageClass has a recovery policy set to Delete. This means that when a PVC is deleted, the corresponding PV (and therefore the data) is also automatically deleted.

To view this policy:

# Afficher la StorageClass avec sa Reclaim Policy
kubectl get sc

# Décrire la StorageClass pour voir les détails
kubectl describe sc standard

Output indicates Reclaim Policy: Delete. This is a behavior to monitor closely in a production environment.


2.9 Add a “retained” StorageClass

The Delete policy is too dangerous for a production Jenkins installation. It is strongly recommended to use the Retain policy, which preserves the PV (and therefore the data) even if the PVC is deleted.

To do this, we create a new StorageClass with the Retain policy:

# Récupérer la définition YAML de la StorageClass existante
kubectl get sc standard -o yaml

We copy this definition, modify it and save it in the manifest/retain.sc.yaml file:

File manifest/retain.sc.yaml:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: retained
provisioner: docker.io/hostpath
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer

We apply this new StorageClass:

kubectl apply -f retain.sc.yaml

# Vérifier les StorageClasses disponibles
kubectl get sc

We now see two StorageClasses: standard (Reclaim Policy: Delete) and retained (Reclaim Policy: Retain).

Modify the StatefulSet to use the new StorageClass

In the jenkins-sts.yaml file, add storageClassName: retained in the volumeClaimTemplates section:

  volumeClaimTemplates:
    - metadata:
        name: jenkins-home
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: retained   # ← Ajout important
        resources:
          requests:
            storage: 1Gi

We reapply the StatefulSet. From now on, the PVC created will use the StorageClass retained, and deleting the PVC will no longer destroy the underlying data.


2.10 Disable Executors on the Controller

After completing the initial Jenkins configuration wizard (install suggested plugins, creating an admin user), we have a functional Jenkins instance in the cluster.

To quickly check that Jenkins is working, you can create a test job:

  1. Go to the Jenkins dashboard.
  2. Create a new job of type Pipeline named hello.
  3. In the pipeline configuration, choose the Hello World script (or paste a preconfigured script).
  4. Click Build Now and verify that the build is successful by consulting the Console Output.

Why disable executors on the Controller?

In a Kubernetes architecture, the Jenkins controller node (the core component) should not run builds. Its role is only to:

  • Serving web interface.
  • Coordinate and orchestrate builds.
  • Communicate with Kubernetes API to create/manage agent pods.

To disable executors on the controller:

  1. Go to Manage JenkinsSystem Configuration (or Configure System).
  2. Find the parameter Number of executors.
  3. Change it to 0.
  4. Click on Apply then Save.

After this modification, if we try to launch a new build, it will remain indefinitely in queue (Build #2 is waiting for next available executor). Indeed, there is no longer any executor available on the controller, and we have not yet configured any external agents.

Build #2 (pipeline hello)
Status: waiting for next available executor

This situation is normal and expected at this point. The rest of the course will show how to add dynamic agents via Kubernetes.


2.11 Destroy everything and lose nothing

To prove that the Retain policy is working correctly, one can perform a complete destruction of all resources and verify that the data survives.

# 1. Surveiller les fichiers sur le nœud (dans un terminal minikube ssh)
ls /tmp/hostpath-provisioner/jenkins/jenkins-home-jenkins-0/

# 2. Supprimer le StatefulSet
kubectl delete -f jenkins-sts.yaml

# 3. Supprimer le PVC
kubectl delete pvc jenkins-home-jenkins-0

# 4. Observer : le PV est maintenant en statut "Released" mais les fichiers existent encore
kubectl get pv

# 5. Supprimer même le PV
kubectl delete pv <nom-du-pv>

Even after deleting the PV, the files remain present on the node because the StorageClass retained does not delete the data. You have to delete the files manually if you really want to clean up.

# 6. Recréer le StatefulSet
kubectl apply -f jenkins-sts.yaml

# 7. Reconnecter le port-forward
kubectl port-forward pod/jenkins-0 8080:8080

# 8. Ouvrir http://localhost:8080
# → Jenkins est de retour avec toute sa configuration intacte !
# → On peut même voir dans la Console Output que les builds reprennent
#   là où ils s'étaient arrêtés (log: "Resuming build")

This demonstrates that the StatefulSet + StorageClass Retain combination provides excellent robustness and resiliency for a Jenkins installation.


3. Dynamic and Scalable Kubernetes Agents

Now that Jenkins is running in the cluster, we will configure the Kubernetes plugin to allow Jenkins to create agent pods on demand. Each build will trigger the creation of a new pod, which will be destroyed at the end of the pipeline.

Target architecture

Jenkins Controller (pod) ─→ API Kubernetes ─→ crée Agent Pod
                                              (le build s'exécute)
                                              ─→ pod supprimé à la fin

3.1 Installing the Kubernetes plugin

The Kubernetes plugin for Jenkins is not installed by default. You have to add it manually.

  1. Go to Manage JenkinsPlugins.
  2. Click on Available plugins.
  3. Search for Kubernetes.
  4. Select the plugin simply named Kubernetes (not “Kubernetes CLI” or other variant).
  5. Click on Install (or Install and restart).

Once the plugin is installed and Jenkins is restarted:

# Reconnecter le port-forward après le redémarrage de Jenkins
kubectl port-forward pod/jenkins-0 8080:8080

In Manage Jenkins, a new option appears: Clouds (or Configure Clouds). This is where we will configure the dynamic agents.


3.2 Grant access to the Kubernetes API

Before configuring the Kubernetes Cloud in Jenkins, you need to understand the necessary permissions.

Test connection

  1. In Manage JenkinsClouds, create a new cloud of type Kubernetes.
  2. Expand Cloud details.
  3. Click on Test Connection (without filling in anything).

The result is an error:

Error: pods is forbidden: User "system:serviceaccount:jenkins:default"
cannot list resource "pods" in API group "" in the namespace "jenkins"

This indicates that Jenkins connects to the Kubernetes API using the service account default of the namespace jenkins, and that this service account does not have permissions to list pods.

Check Jenkins pod service account

# Récupérer le YAML du pod jenkins-0 et chercher serviceAccountName
kubectl get pod jenkins-0 -o yaml | grep serviceAccount

We confirm that the pod uses the service account default.

Solution: bind a ClusterRole to the service account

To simplify in this demonstration context, we will use the ClusterRole cluster-admin (total access). In production, you would need to create a Role or ClusterRole with the minimum necessary permissions.

# Lister les ClusterRoles disponibles
kubectl get clusterroles

# Voir les règles du ClusterRole cluster-admin
kubectl describe clusterrole cluster-admin

We create a manifest/rbac.yaml file to link the account service to the ClusterRole:

File manifest/rbac.yaml:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: jenkins-cluster-admin
subjects:
  - kind: ServiceAccount
    name: default
    namespace: jenkins
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io
kubectl apply -f rbac.yaml

3.3 Create a Pod Template

After granting permissions, you can configure the Cloud in Jenkins:

  1. Return to Manage JenkinsClouds → configure the Kubernetes cloud.
  2. Click Test Connection again. This time the connection is successful.
  3. Click on Save.
  4. Reopen the configuration and go to Pod templates.
  5. Click on Add a pod template.
  6. Configure:
  • Name: test
  • Namespace: jenkins
  • Usage: Use this node as much as possible
  1. Click on Create.

As soon as this configuration is saved, Jenkins sees that there is a pending build (the hello job blocked since the end of module 2) and immediately creates an agent pod.

# Observer les pods créés
kubectl get pods

We see a new pod whose name is prefixed with test- (the template name), for example test-abc12-def34. This pod is running the Jenkins build.


3.4 Connect Agent Pod to Port 8080

When examining the agent pod logs, we notice an error:

Error: the agent tried to connect to localhost:8080
→ Connection refused

The agent pod is trying to connect to localhost:8080 to communicate with the Jenkins controller. But localhost in the context of a Kubernetes pod refers to the pod itself, not the Jenkins pod.

The agent must be able to discover Jenkins by its service DNS name in the cluster.

Create a Service for Jenkins

File manifest/jenkins.svc.yaml (first part):

apiVersion: v1
kind: Service
metadata:
  name: jenkins
  namespace: jenkins
spec:
  selector:
    app: jenkins
  ports:
    - port: 8080
      targetPort: 8080
kubectl apply -f jenkins.svc.yaml

# Vérifier que le service est créé
kubectl get services

To test from inside an agent pod:

# Ouvrir un shell dans le pod agent
kubectl exec -it <nom-du-pod-agent> -- bash

# Dans le pod agent, tester la connexion au service jenkins
curl http://jenkins:8080

The response is a redirect to the Jenkins login page — the service is indeed accessible from the pod agent via Kubernetes DNS service discovery.

Configure the Jenkins URL in the template

In the Kubernetes Cloud configuration, under Pod templates → template testConfigure:

  • Set Jenkins URL: http://jenkins:8080

3.5 Connect Agent Pod to Port 50000

A new error appears:

Error: the provided port 50000 is not reachable

Port 50000 is the JNLP (Java Network Launch Protocol) port, used by Jenkins agents to establish an inbound connection to the controller. There also needs to be a service for this port.

Add the agent service in the same YAML file

File manifest/jenkins.svc.yaml (complete):

apiVersion: v1
kind: Service
metadata:
  name: jenkins
  namespace: jenkins
spec:
  selector:
    app: jenkins
  ports:
    - port: 8080
      targetPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: jenkins-agent
  namespace: jenkins
spec:
  selector:
    app: jenkins
  ports:
    - port: 50000
      targetPort: 50000
kubectl apply -f jenkins.svc.yaml

# Tester depuis un pod agent
kubectl exec -it <pod-agent> -- bash
curl http://jenkins-agent:50000

Configure the Kubernetes Cloud Tunnel

In the Kubernetes Cloud configuration, under Pod templatesConfigure of the cloud (not the template):

  • Jenkins URL: http://jenkins:8080
  • Jenkins tunnel: jenkins-agent:50000
# Supprimer le pod agent existant pour en créer un nouveau avec la bonne config
kubectl delete pod <pod-agent>

The new agent pod is connecting successfully. The pending build runs and completes successfully.


3.6 Observe the Life Cycle of a Pod

To better understand the life cycle of a pod agent, you can use the kubectl --watch flag:

# Observer les pods en temps réel avec leurs labels
kubectl get pods --watch --show-labels

From the Jenkins dashboard, we trigger a new build and observe:

  1. The pod is created (status Pending).
  2. It goes to Running.
  3. The build runs (~10 seconds for the hello pipeline).
  4. It goes to Terminating.
  5. He disappears.

We can also observe the events of the namespace to have a history:

kubectl get events

The events show:

  • Checking the image.
  • Creation of the container.
  • Starting the container.
  • Stopping the container.

3.7 Trigger 3 Simultaneous Builds

The main advantage of Kubernetes dynamic agents is their automatic horizontal scalability. If you trigger several builds at the same time, Kubernetes creates as many pods as there are builds:

# Regarder les pods pendant qu'on déclenche des builds
kubectl get pods --watch --show-labels

By triggering 3 builds from the Jenkins dashboard:

Build 1 → Pod agent 1 créé (Running)
Build 2 → Pod agent 2 créé (Running)
Build 3 → Pod agent 3 créé (Running)
→ Chacun s'exécute indépendamment
→ Chacun se termine et son pod est supprimé
→ On revient à un seul pod (le controller jenkins-0)

This is a striking demonstration of the elastic scalability that Kubernetes brings to Jenkins.

Select logs by label rather than by name

Since the pod name changes with each build, it is more convenient to use a label selector to view the logs:

# Sélectionner le pod agent par son label (jenkins=slave)
# et suivre les logs en temps réel
kubectl logs -l jenkins=slave --follow

3.8 Delay Pod Termination

By default, the agent pod is deleted immediately after the build completes. This can make debugging difficult. You can configure a retention period.

In the configuration of the Pod template test:

  1. Scroll to the Pod Retention section.
  2. Find the parameter Time in minutes to retain agent when idle (or similar).
  3. Set it to 1 (one minute).
  4. Click on Save.

Trigger a new build and observe: After the build completes, the pod remains present for about a minute before being deleted.

This allows time to inspect the status of the pod, its logs, or its file system before it disappears.

There is also the Pod Retention option which allows you to retain a pod only in case of failure, but this requires manual deletion afterwards. The idle setting is more convenient for debugging.


3.9 Reuse of Pods for multiple Builds

An interesting behavior arises from the retention setting: if a pod is still alive (idle) when a new build starts, Jenkins will reuse this pod rather than create a new one.

To observe this behavior:

  1. Trigger a build, wait for it to complete.
  2. During the one minute window (pod still alive), trigger another build.
# Observer le nom des pods (il ne devrait pas changer)
kubectl get pods --watch

We see that the second build (and the third if we trigger another in the same window) are running on the same pod as the first. The pod is shared between successive builds during its inactivity period.

To confirm: look at the name of the pod (e.g. test-abc12-vq545). This name remains the same for builds 14, 15 and 16 if they are triggered quickly.

To easily navigate between builds, the trick is to stick to the /lastBuild/ URL rather than a fixed build number. By refreshing the page, we always see the latest build and its current status.


4. Use Pods in Pipelines

In this module, we learn how to customize the agent pod directly in the pipeline script, rather than relying solely on the templates configured globally in Jenkins. This allows the pod to be tailored to each pipeline specifically.


4.1 Custom Pod Template in a Declarative Pipeline

When the hello pipeline is configured with agent any, it uses the global pod template called test (configured in the Kubernetes Cloud). This can be recognized by the addition of the test- prefix to the created pod.

By modifying the pipeline to specify a custom Kubernetes agent, the pod template becomes pipeline specific. The pod will then have the pipeline name as a prefix.

Pipeline hello — before (global agent):

pipeline {
  agent any
  stages {
    stage('Hello') {
      steps {
        echo 'Hello, World!'
        sleep 5
        echo 'Halfway'
        sleep 5
        echo 'Ended'
      }
    }
  }
}

Pipeline hello — after (custom Kubernetes agent):

pipeline {
  agent {
    kubernetes {
      // Pas de YAML pour l'instant → utilise le pod par défaut (jnlp)
    }
  }
  stages {
    stage('Hello') {
      steps {
        echo 'Hello, World!'
        sleep 5
        echo 'Halfway'
        sleep 5
        echo 'Ended'
      }
    }
  }
}

After this change:

  • The pod is now prefixed with hello- (the pipeline name).
  • The pod no longer respects the one minute retention time of the global template.

These two differences confirm that the pipeline now uses its own inline pod template.


4.2 Add a Maven Container

To execute an mvn command in the pipeline, you must add a container based on the Maven image to the pod.

If we try to run mvn --version without adding the Maven image:

steps {
  sh 'mvn --version'  // ← Échec : commande introuvable
}

The build fails with the error mvn: command not found.

Use Jenkins Syntax Builder

To facilitate the generation of the YAML configuration, Jenkins offers the Pipeline Syntax (link at the bottom of the pipeline configuration page) → Declarative Directive Generator:

  1. Select the agent directive.
  2. Choose type Kubernetes.
  3. In the YAML field, paste the pod spec:
apiVersion: v1
kind: Pod
spec:
  containers:
    - name: maven
      image: maven:3.8-eclipse-temurin-11
      command:
        - sleep
      args:
        - infinity

Why sleep infinity? The Maven container should not shut down by itself. By making it sleep indefinitely, it remains active as long as the pod exists, allowing Jenkins to run commands on it on demand.

  1. Click on Generate then Copy.

We obtain the complete agent { kubernetes { ... } } block to paste into the pipeline.


4.3 container() — Run mvn in Maven Container

To execute a command in a specific container of the pod (other than the jnlp agent container), we use the container() function:

hello pipeline with Maven container:

pipeline {
  agent {
    kubernetes {
      yaml """
apiVersion: v1
kind: Pod
spec:
  containers:
    - name: maven
      image: maven:3.8-eclipse-temurin-11
      command:
        - sleep
      args:
        - infinity
"""
    }
  }
  stages {
    stage('Hello') {
      steps {
        container('maven') {
          sh 'mvn --version'
        }
      }
    }
  }
}

After execution:

  • Maven version is displayed correctly in the Output Console.
  • Kubernetes events show that the Maven image has been downloaded (pull), the Maven container has been created and started, then stopped at the end of the build.
  • The pod contains two containers: maven (our container) and jnlp (the Jenkins agent).

Tip: To see the final YAML of the pod (with default values ​​+ custom values ​​merged), you can consult the classic Output Console (not the Pipeline Console). At the start of the release, we find the complete spec pod with all the containers.


4.4 podTemplate() in a Scripted Pipeline

Until now, we have worked with the Declarative Pipeline syntax. There is a second syntax: Scripted Pipeline, which offers more flexibility.

To create a scripted pipeline, you create a new job from the dashboard (for example by copying hello and renaming it scripted).

Pipeline scripted — Scripted equivalent of the previous Declarative:

podTemplate(yaml: """
apiVersion: v1
kind: Pod
spec:
  containers:
    - name: jdk
      image: eclipse-temurin:17
      command:
        - sleep
      args:
        - infinity
""") {
  node(POD_LABEL) {
    stage('Hello') {
      container('jdk') {
        sh 'java -version'
      }
    }
  }
}

Important points:

  • podTemplate(...) configures the type of pod to use.
  • node(POD_LABEL) allocates a workspace and executes code in a node (the pod). Without node(), there is no workspace: impossible to retrieve code or produce files.
  • POD_LABEL is a variable automatically set by the Kubernetes plugin to designate the label of the pod created by the enclosing podTemplate.

4.5 Checkout of a Git repository in the Pod Workspace

The node(POD_LABEL) allocates an empty workspace. To place code there, we use the git step:

podTemplate(yaml: """
apiVersion: v1
kind: Pod
spec:
  containers:
    - name: jdk
      image: eclipse-temurin:17
      command: [sleep]
      args: [infinity]
""") {
  node(POD_LABEL) {
    stage('checkout') {
      // Inspecter le workspace (vide au départ)
      sh 'pwd && ls -la'

      // Récupérer le code source depuis Git
      git branch: 'end',
          url: 'https://github.com/g0t4/course-jenkins-k8s-spc'

      // Lister les fichiers après le checkout
      sh 'ls -la'
    }
    stage('package') {
      container('jdk') {
        sh './mvnw --batch-mode --no-transfer-progress package'
      }
    }
  }
}

The repository used is a version of the Spring PetClinic project prepared for the course. The end branch corresponds to the final state of the code after the Jenkins startup course.


4.6 A JDK Container with the Eclipse Temurin image

To build a Java project with Maven Wrapper (./mvnw), you only need a JDK (the Maven Wrapper is already included in the project). We replace the Maven image with the Eclipse Temurin image (high quality JDK distribution based on OpenJDK).

podTemplate(yaml: """
apiVersion: v1
kind: Pod
spec:
  containers:
    - name: jdk
      image: eclipse-temurin:17
      command: [sleep]
      args: [infinity]
""") {
  node(POD_LABEL) {
    stage('checkout') {
      git url: 'https://github.com/g0t4/course-jenkins-k8s-spc'
    }
    stage('package') {
      container('jdk') {
        sh './mvnw --batch-mode --no-transfer-progress package'
      }
    }
  }
}

Note: The --batch-mode parameter disables interactive messages. --no-transfer-progress (or -ntp) removes dependency download progress bars, making console output lighter.

A first failure may occur because of a failing unit test in the original project. The solution: use the corrected repository (course-jenkins-k8s-spc, branch master). After correction, the build succeeds.


4.7 Exec in Pod to inspect Workspace

When we want to inspect what is happening in the pod during a build, we can use kubectl exec to open an interactive shell in the pod:

# Identifier le pod en cours d'exécution (ex: scripted-6)
kubectl get pods

# Ouvrir un shell dans le container jdk du pod
kubectl exec -it scripted-6 -c jdk -- bash

Note: The pod name follows the pattern <job-name>-<build-number>. For example, scripted-6 corresponds to build number 6 of job scripted.

To add a delay in the pipeline to allow time to inspect:

stage('package') {
  container('jdk') {
    sh './mvnw --batch-mode --no-transfer-progress package'
    sleep 60  // ← Pause d'une minute pour inspecter
  }
}

Find the workspace in the pod

# Obtenir le YAML du pod pour trouver le chemin du volume workspace
kubectl get pod scripted-6 -o yaml

# Dans la sortie YAML, chercher "workspace-volume" et son mountPath
# Ex: /home/jenkins/agent/workspace/scripted

# Dans le shell exec
cd /home/jenkins/agent/workspace/scripted
ls -la
# → Le code source et les artefacts de build sont visibles ici

The two containers of the pod (jdk and jnlp) share the same workspace volume. It is possible to exec in either, as they both have access to the same files.


4.8 Run Capture step

After a successful build, we want to capture the artifacts produced (JAR, test reports, code coverage) to archive them in Jenkins.

stage('capture') {
  // Archiver le JAR produit
  archiveArtifacts artifacts: 'target/*.jar', fingerprint: true

  // Publier les résultats des tests JUnit
  junit 'target/surefire-reports/*.xml'

  // Capturer la couverture de code (nécessite le plugin Coverage)
  recordCoverage(tools: [[parser: 'JACOCO']])
}

Note: The recordCoverage step requires the Coverage plugin (formerly JaCoCo Plugin). Without this plugin, the build fails with the error No such DSL method 'recordCoverage'. We install it in the next section.

After installing the Coverage plugin and a new build, the artifacts are visible in the build dashboard:

  • A download link for the .jar file.
  • A test report (with results per package, execution time, etc.).
  • A code coverage report with detailed statistics.

4.9 DSLs: containerTemplate() + persistentVolumeClaim()

Downloading Maven dependencies with each build can take a long time. One solution is to share a Maven repository between multiple builds via a PersistentVolumeClaim.

There are two approaches to configuring volumes in the pod:

  1. Pure YAML: Define a volumeMount and a volume of type persistentVolumeClaim directly in the YAML of the pod spec.
  2. DSL Jenkins: Use the containerTemplate() and persistentVolumeClaim() functions provided by the Kubernetes plugin.

We create a new pipeline scripted-pvc based on a copy of scripted, using the DSL approach:

podTemplate(
  containers: [
    containerTemplate(
      name: 'jdk',
      image: 'eclipse-temurin:17',
      command: 'sleep',
      args: 'infinity'
    )
  ],
  volumes: [
    persistentVolumeClaim(
      claimName: 'shared-maven-repo',
      mountPath: '/root/.m2/repository'
    )
  ]
) {
  node(POD_LABEL) {
    stage('checkout') {
      git url: 'https://github.com/g0t4/course-jenkins-k8s-spc'
    }
    stage('package') {
      container('jdk') {
        sh './mvnw --batch-mode --no-transfer-progress package'
      }
    }
    stage('capture') {
      archiveArtifacts artifacts: 'target/*.jar'
      junit 'target/surefire-reports/*.xml'
      recordCoverage(tools: [[parser: 'JACOCO']])
    }
  }
}

Note: The containerTemplate() function generates a container in the pod spec. It takes the same parameters as a YAML container (name, image, command, args). The persistentVolumeClaim() function mounts a PVC in the container at a given path.


4.10 Pod waiting until PVC created

When first launching the scripted-pvc pipeline, the pod remains in Pending state indefinitely. When describing the pod, we see the explanation in the events:

kubectl describe pod scripted-pvc-1

In the Events section:

Warning  FailedScheduling  pod/scripted-pvc-1  0/1 nodes are available:
1 Insufficient memory. preemption: 0/1 nodes are available:
persistentvolumeclaim "shared-maven-repo" not found

The pod cannot start because the PVC shared-maven-repo does not exist. You have to create it manually.

File manifest/pvc.maven-repo.yaml:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: shared-maven-repo
  namespace: jenkins
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: retained   # Utiliser la StorageClass "retained" pour ne pas perdre le cache
  resources:
    requests:
      storage: 5Gi
kubectl apply -f pvc.maven-repo.yaml

Immediately afterwards, the pod goes into ContainerCreating state then Running.

Result

  • First build: Maven downloads all dependencies → 1 minute 33 seconds.
  • Second build: Maven uses local cache → 17 seconds only.

We saved 1 minute 15 seconds on each subsequent build. In production with many projects and dependencies, the gain can be much more significant.

Inspection on the node: Via minikube ssh, we can see the Maven dependencies accumulating in the shared-maven-repo directory on the node:

minikube ssh
ls /tmp/hostpath-provisioner/jenkins/shared-maven-repo/
# → We see the Maven groupIds (eg: org, com, io...) being added gradually

4.11 Multiple Containers in Parallel in a Pod

A pod can contain several containers operating simultaneously. You can use both styles (YAML and DSL containerTemplate) by mixing them.

versions.groovy file (demo pipeline):

podTemplate(
  yaml: """
apiVersion: v1
kind: Pod
spec:
  containers:
    - name: maven
      image: maven:3.8-eclipse-temurin-11
      command: [sleep]
      args: [infinity]
    - name: maven38
      image: maven:3.8
      command: [sleep]
      args: [infinity]
    - name: git
      image: alpine/git
      command: [sleep]
      args: [infinity]
""",
  containers: [
    containerTemplate(name: 'node',   image: 'node:lts',
                      command: 'sleep', args: 'infinity'),
    containerTemplate(name: 'python', image: 'python:3',
                      command: 'sleep', args: 'infinity'),
    containerTemplate(name: 'dotnet', image: 'mcr.microsoft.com/dotnet/sdk:6.0',
                      command: 'sleep', args: 'infinity')
  ]
) {
  node(POD_LABEL) {
    stage('versions') {
      parallel(
        maven:   { container('maven')   { sh 'mvn --version'    } },
        maven38: { container('maven38') { sh 'mvn --version'    } },
        git:     { container('git')     { sh 'git --version'    } },
        node:    { container('node')    { sh 'node --version'   } },
        python:  { container('python')  { sh 'python --version' } },
        dotnet:  { container('dotnet')  { sh 'dotnet --version' } }
      )
    }
  }
}

This pipeline demonstrates:

  1. The mixture of the two styles of pod configuration (YAML + containerTemplate).
  2. Running parallel branches in a single pod with multiple containers.
  3. Each parallel branch runs in its dedicated container (with its specific tool and version).

Key advantage: There is no longer any need to create or maintain static Jenkins agents with a particular combination of tools. We simply declare the necessary images in the pipeline.


4.12 Multiple Pods in Parallel

For more complex pipelines, we can use several distinct pods (and therefore several Kubernetes nodes), each having its own environment.

parallel.groovy file (full architecture):

// Pod de build avec un container JDK personnalisé
podTemplate(name: 'build', yaml: """
apiVersion: v1
kind: Pod
spec:
  terminationGracePeriodSeconds: 2
  containers:
    - name: jdk
      image: eclipse-temurin:17
      command: [sleep]
      args: [infinity]
""") {
  node(POD_LABEL) {
    stage('build-it') {
      // Pod imbriqué (nested) qui tourne pendant le pod "build"
      podTemplate(name: 'nested') {
        node(POD_LABEL) {
          echo 'Je suis le pod nested'
        }
      }
    }

    // Stages parallèles dans des pods séparés
    parallel(
      'test-a': {
        podTemplate(name: 'test-a') {
          node(POD_LABEL) {
            stage('Test A') { echo 'Tests A en cours...' }
          }
        }
      },
      'test-b': {
        podTemplate(name: 'test-b') {
          node(POD_LABEL) {
            stage('Test B') { echo 'Tests B en cours...' }
          }
        }
      }
    )

    // Pod de capture à la fin
    podTemplate(name: 'capture') {
      node(POD_LABEL) {
        stage('capture') {
          echo 'Capture des résultats'
        }
      }
    }
  }
}

What we observe when monitoring pods:

1. Pod "build"    → démarrage de la phase de build
2. Pod "nested"   → démarre pendant que "build" tourne (pod imbriqué)
3. Pod "test-a"   → démarre en parallèle avec "test-b"
4. Pod "test-b"   → démarre en parallèle avec "test-a"
5. Pod "capture"  → démarre après la fin des tests

Architectural flexibility:

  • Pods can be nested (a pod creates another pod in its node() block).
  • Parallel pods are running concurrently on the cluster.
  • Each pod can have its own containers, images and resources.

4.13 Clear stuck Pods with terminationGracePeriodSeconds

We observe that the pods in the build stage (with the Eclipse Temurin image) take a long time to complete after the build has finished — around 30 seconds. This is due to Kubernetes’ default graceful termination timeout (30 seconds).

To reduce this delay, we can add terminationGracePeriodSeconds in the pod spec:

podTemplate(name: 'build', yaml: """
apiVersion: v1
kind: Pod
spec:
  terminationGracePeriodSeconds: 2   # ← Réduction du délai à 2 secondes
  containers:
    - name: jdk
      image: eclipse-temurin:17
      command: [sleep]
      args: [infinity]
""") {
  // ...
}

Result: Pods with JDK image now complete in ~2 seconds instead of ~30 seconds.

Why is the Temurin image taking a long time? The Eclipse Temurin image (Java JVM) probably has longer shutdown logic than lightweight images. The terminationGracePeriodSeconds gives him time to finish cleanly before being forcibly killed.

Propagating to nested pods: The definition of terminationGracePeriodSeconds in the podTemplate of build also applies to the nested pod, as they share the same parent configuration. This is a behavior to keep in mind.


5. Use JCasC with the Chart Helm

In this module, we abandon manual management by manifests to adopt the official Jenkins Helm chart. This chart allows for a single command, fully configured deployment with native support for Kubernetes agents.

What is Helm?

Helm is the package manager for Kubernetes. A chart is a set of files that describe a complete Kubernetes application. The official Jenkins chart automatically configures everything that was done manually in previous modules:

  • StatefulSet + PVC for the controller.
  • Services (port 8080 and port 50000).
  • Service Account and RBAC.
  • Pre-installed and configured Kubernetes plugin.
  • Configuration as Code (JCasC) to inject configuration without a wizard.

5.1 Switch to a new Minikube Cluster

To start again on a clean basis, we create a second minikube cluster with the helm profile:

# Lister les profils existants
minikube profile list

# Arrêter le cluster "manifest" pour éviter les conflits de ressources
minikube stop --profile=manifest

# Vérifier que le cluster est bien arrêté
minikube profile list

# Démarrer un nouveau cluster avec le profil "helm"
minikube start --nodes=1 --profile=helm

# Sélectionner ce profil
minikube profile helm

# Vérifier le contexte actif
kubectl config get-contexts
# → Contexte "helm" avec namespace "default" (pas encore de namespace jenkins)

5.2 Add Jenkins repository to Helm

Before installing the Jenkins chart, you must add the official Helm repository of Jenkins:

# Vérifier les dépôts actuels (aucun par défaut)
helm repo list

# Ajouter le dépôt Jenkins
helm repo add jenkins https://charts.jenkins.io

# Mettre à jour la liste des charts disponibles
helm repo update

# Vérifier que le dépôt a bien été ajouté
helm repo list

# Rechercher les charts disponibles dans ce dépôt
helm search repo jenkins

# → Résultat typique :
# NAME             CHART VERSION  APP VERSION
# jenkins/jenkins  5.1.6          2.440.3-lts  ← Version LTS la plus récente

5.3 Reinstall in Jenkins Namespace

First time installation (namespace default)

# Installation simple dans le namespace default
helm install jenkins1 jenkins/jenkins

Output shows usage notes (Jenkins chart specific):

# Récupérer le mot de passe admin (généré aléatoirement)
kubectl exec --namespace default -it svc/jenkins1 -c jenkins -- \
  /bin/cat /run/secrets/additional/chart-admin-password && echo

# Ouvrir l'UI via port-forward
kubectl --namespace default port-forward svc/jenkins1 8080:8080

We connect to http://localhost:8080 with admin and the recovered password. Jenkins is up and running without a configuration wizard.

Reinstallation in a dedicated namespace

# Désinstaller l'installation dans default
helm list
helm uninstall jenkins1

# Vérifier que tout est supprimé
helm list
# → Aucune release

# Réinstaller dans le namespace jenkins avec création automatique du namespace
helm install jenkins1 jenkins/jenkins \
  --namespace jenkins \
  --create-namespace

# Vérifier les namespaces
kubectl get namespaces
# → namespace "jenkins" créé automatiquement

# Voir les ressources dans le namespace jenkins
kubectl get all -n jenkins

# Récupérer les notes de l'installation pour le port-forward
# (le mot de passe est différent car c'est une nouvelle instance)

5.4 Install a Second Jenkins Instance

One of the major advantages of Helm is the ease of multiplying installations:

# Installer une deuxième instance dans un namespace séparé
helm install jenkins2 jenkins/jenkins \
  --namespace jenkins2 \
  --create-namespace

# Vérifier les namespaces
kubectl get namespaces
# → jenkins   (instance 1)
# → jenkins2  (instance 2)

# Lister toutes les releases Helm
helm list --all-namespaces
# → jenkins1  (namespace jenkins)
# → jenkins2  (namespace jenkins2)

# Récupérer les notes de la deuxième instance
helm get notes jenkins2 --namespace jenkins2

# Port-forward pour la deuxième instance sur un port différent
kubectl --namespace jenkins2 port-forward svc/jenkins2 8090:8080

We can open http://localhost:8090 to access the second instance. To confirm that this is indeed a separate instance:

  1. In instance 1 (port 8080): create a test-pipeline item.
  2. In instance 2 (port 8090): check that test-pipeline does not exist → the two instances are well isolated.

5.5 Kubernetes Pod Agents available out of the box

To test that Kubernetes agents work immediately:

  1. In the test instance, configure a pipeline with custom containers (copy one of the pipelines from module 4).
  2. Click Build Now.
# Observer les pods pendant l'exécution du build
kubectl get pods -n jenkins --watch

An agent pod is automatically created and the build runs.

What was needed manually in module 3 and which is already configured by the chart:

  • ✅ Kubernetes plugin installed.
  • ✅ Kubernetes cloud configured with jenkinsUrl and jenkinsTunnel.
  • ✅ Default Pod template (with jnlp container) defined.
  • ✅ Service account with the correct RBAC permissions.

5.6 Examine Created Resources

To understand what the Helm chart creates, we can use the helm template command which generates the YAML manifest without applying it:

# Générer et afficher tous les manifests que Helm déploierait
helm template jenkins1 jenkins/jenkins \
  --namespace jenkins | bat  # bat pour la colorisation

The output contains many resources. To get an overview:

# Filtrer pour voir uniquement les types de ressources (Kind)
helm template jenkins1 jenkins/jenkins \
  --namespace jenkins \
  | grep "^kind:" | sort | uniq

Typical result:

kind: ClusterRole
kind: ClusterRoleBinding
kind: ConfigMap
kind: Deployment
kind: PersistentVolumeClaim
kind: Pod
kind: Role
kind: RoleBinding
kind: Secret
kind: Service
kind: ServiceAccount
kind: StatefulSet

To inspect the resources actually deployed in the namespace:

# Lister toutes les ressources dans le namespace jenkins (release jenkins1)
kubectl get pvc,pods,services,serviceaccount,configmap,secret \
  --namespace jenkins

We find familiar resources:

  • PersistentVolumeClaim → for jenkins_home.
  • Pods → the controller and a possible init container.
  • Services → one on port 8080 (UI) and one on port 50000 (agents).
  • ServiceAccountjenkins1 (specific to this release, not default).
  • ConfigMaps → containing bootstrap configurations.
  • Secrets → containing the admin password.

5.7 ConfigMaps define how to start Jenkins

Two ConfigMaps are particularly important:

ConfigMap jenkins1 — startup script and plugins

kubectl get configmap jenkins1 -n jenkins -o yaml

In the data section, we find:

  • apply_config.sh: a shell script executed at startup that:

  • Disables the setup wizard (SKIP_SETUP_WIZARD).

  • Installs the plugins listed in plugins.txt.

  • plugins.txt: the list of plugins to install, for example:

kubernetes:latest
workflow-aggregator:latest
git:latest
configuration-as-code:latest
...

ConfigMap jenkins1-jcasc-config — Configuration as Code (JCasC)

kubectl get configmap jenkins1-jcasc-config -n jenkins -o yaml

This ConfigMap contains a JCasC configuration YAML document. This is why Jenkins is already configured from the start. There we find in particular:

jenkins:
  clouds:
    - kubernetes:
        name: kubernetes
        jenkinsUrl: http://jenkins1:8080
        jenkinsTunnel: jenkins1-agent:50000
        templates:
          - name: default
            namespace: jenkins
            containers:
              - name: jnlp
                image: jenkins/inbound-agent:latest
                ...

Thanks to this ConfigMap, the Kubernetes plugin is already configured with:

  • The URL of the Jenkins controller (service jenkins1 on port 8080).
  • The agent tunnel (service jenkins1-agent on port 50000).
  • A default pod template with the jnlp container.

5.8 Specify Additional Plugins

To add plugins, we use the controller.additionalPlugins parameter of the chart.

Chart documentation is available at ArtifactHub. You can search for the available parameters:

  • In values.yaml: brief explanations of each parameter.
  • In values.md: complete list with dot notation, links and default values.

To add plugins, create a values ​​file values/additional-plugins.yaml:

File values/additional-plugins.yaml:

controller:
  additionalPlugins:
    - pipeline-graph-view:latest
    - jacoco:latest
    - coverage:latest
    - configuration-as-code:latest
    - job-dsl:latest

Note: The controller.additionalPlugins parameter is added to the list of plugins already defined by the chart. It does not replace core plugins (like Kubernetes, Git, etc.).


5.9 Update Chart Values ​​to add Plugins

To update an existing installation with new values, use helm upgrade:

# Voir l'état actuel du ConfigMap (avant la mise à jour)
kubectl get configmap jenkins1 -n jenkins -o yaml

# Mettre à jour le déploiement avec les plugins supplémentaires
helm upgrade jenkins1 jenkins/jenkins \
  --namespace jenkins \
  --values values/additional-plugins.yaml

# Surveiller la progression (Jenkins redémarre pour appliquer les changements)
kubectl get pods -n jenkins --watch

During the update, you can check that the ConfigMap is updated:

kubectl get configmap jenkins1 -n jenkins -o yaml
# → La section plugins.txt contient maintenant les nouveaux plugins

After restarting Jenkins and reconnecting port-forward:

kubectl --namespace jenkins port-forward svc/jenkins1 8080:8080

In the Jenkins interface → Manage JenkinsInstalled plugins, we find the new plugins (eg: pipeline-graph-view).


5.10 Change Admin Password

To define a fixed password (useful in a learning or development environment):

File values/admin-pass.yaml:

controller:
  adminPassword: admins

⚠️ Warning: Never store a clear password in a versioned file in production. Use Kubernetes Secrets or a secrets manager (Vault, AWS Secrets Manager, etc.).

It is convenient to create a helm/deploy.sh script which combines all the value files:

File helm/deploy.sh:

#!/bin/bash
helm upgrade --install jenkins1 jenkins/jenkins \
  --namespace jenkins \
  --create-namespace \
  --values values/additional-plugins.yaml \
  --values values/admin-pass.yaml

Note: The --install argument to helm upgrade --install creates the installation if it does not yet exist, or updates if it does. This is a convenient idempotent command.

After executing the script, if the password does not apply immediately, you must force a restart of the StatefulSet:

kubectl rollout restart statefulset/jenkins1 -n jenkins

Once Jenkins is restarted and the port-forward reconnected, you can connect with admin / admins.


5.11 Apply Dark Theme via JCasC

Configuration as Code (JCasC) allows you to inject any Jenkins configuration via a YAML file, without using the graphical interface.

To observe the current configuration:

  1. Go to Manage JenkinsConfiguration as Code.
  2. Click on View Configuration.

This results in a long YAML document representing the current state of Jenkins. We can see, among other things, the appearance section with themeManager.

To enable the dark theme:

File values/configscripts-dark-theme.yaml:

controller:
  JCasC:
    configScripts:
      dark-theme: |
        appearance:
          themeManager:
            disableUserThemes: true
            theme: "dark"

File values/jenkins-url.yaml:

controller:
  JCasC:
    configScripts:
      jenkins-url: |
        unclassified:
          location:
            url: http://localhost:8080/

Note: The Jenkins URL is required for dark theme CSS assets to load correctly.

We add these files to the deployment script:

#!/bin/bash
helm upgrade --install jenkins1 jenkins/jenkins \
  --namespace jenkins \
  --create-namespace \
  --values values/additional-plugins.yaml \
  --values values/admin-pass.yaml \
  --values values/configscripts-dark-theme.yaml \
  --values values/jenkins-url.yaml

After running, Jenkins automatically switches to dark theme without restarting. To confirm in JCasC:

  1. Manage JenkinsConfiguration as CodeReload existing configuration.
  2. Search for appearance in the displayed configuration.
  3. We see themeManager with theme: dark and disableUserThemes: true.

5.12 The /var/jenkins_home/casc_configs directory

JCasC ConfigMaps are mounted as files in the /var/jenkins_home/casc_configs/ directory of the Jenkins container.

# Observer les ConfigMaps et pods en temps réel
kubectl get configmap,pods -n jenkins --watch

# Exec dans le pod Jenkins
kubectl exec -it jenkins1-0 -n jenkins -c jenkins -- bash

# Dans le shell du pod
cd /var/jenkins_home/casc_configs/
ls -la
# → dark-theme.yaml
# → jcasc-default-config.yaml  (le grand document de config JCasC par défaut)

# Afficher le fichier du thème sombre
cat dark-theme.yaml
# → Contenu identique à ce qu'on a défini dans le ConfigMap

# Afficher le fichier de configuration par défaut
cat jcasc-default-config.yaml
# → Long document avec cloud, templates, etc.

The correspondence between the file name (dark-theme.yaml) and the key name in the ConfigMap (dark-theme) is direct. This is the Kubernetes mount mechanism (volumes.configMap.items).


5.13 The Jenkins Multi-Container Pod Controller

The Jenkins pod created by the Helm chart contains several containers:

kubectl describe pod jenkins1-0 -n jenkins

We discover:

Init Containers (executed before starting the main container)

  1. config-reload-init: Performs a first load of the JCasC ConfigMaps in the casc_configs directory before Jenkins starts.

Regular Containers (run in parallel)

  1. jenkins: The main Jenkins controller.
  • Mount: /var/jenkins_home → PVC.
  • Mount: /var/jenkins_home/casc_configs → JCasC ConfigMaps.
  1. config-reload: A sidecar container which:
  • Continuously monitors changes in ConfigMaps with a specific label.
  • Copy changes to casc_configs directory.
  • Calls the Jenkins API to trigger a configuration reload (POST /reload-configuration-as-code/).

This mechanism allows Jenkins to react dynamically to ConfigMaps changes without requiring a pod restart.

# Extrait du pod spec montrant le container config-reload
containers:
  - name: config-reload
    image: kiwigrid/k8s-sidecar:latest
    env:
      - name: LABEL
        value: jenkins1-jenkins-config   # Label des ConfigMaps à surveiller
      - name: FOLDER
        value: /var/jenkins_home/casc_configs
      - name: NAMESPACE
        value: jenkins
      - name: REQ_URL
        value: http://localhost:8080/reload-configuration-as-code/?casc-reload-token=...

5.14 Seeding a Job Pipeline

To go even further, you can create Jenkins jobs automatically via JCasC, using the Job DSL plugin.

File values/configscripts-versions-job.yaml:

controller:
  JCasC:
    configScripts:
      versions-job: |
        jobs:
          - script: |
              pipelineJob('versions') {
                definition {
                  cps {
                    script("""
podTemplate(yaml: '''
apiVersion: v1
kind: Pod
spec:
  containers:
    - name: maven
      image: maven:3.8-eclipse-temurin-11
      command: [sleep]
      args: [infinity]
''') {
  node(POD_LABEL) {
    stage('versions') {
      container('maven') {
        sh 'mvn --version'
      }
    }
  }
}
""")
                    sandbox(false)
                  }
                }
              }

File values/configscripts-welcome-message.yaml:

controller:
  JCasC:
    configScripts:
      welcome-message: |
        jenkins:
          systemMessage: "Bienvenue dans Jenkins sur Kubernetes !"

To disable script security (demo only, not production):

File values/configscripts-disable-script-security.groovy (init script):

// Montage en tant que script init dans /var/jenkins_home/init.groovy.d/
import jenkins.model.Jenkins
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval

def approval = ScriptApproval.get()
// Désactiver la vérification de sécurité pour le Job DSL
Jenkins.instance.getDescriptorByType(
  org.jenkinsci.plugin.dsl.GlobalDslSandboxSetting
).setSandboxEnabled(false)

Final deployment script:

#!/bin/bash
helm upgrade --install jenkins1 jenkins/jenkins \
  --namespace jenkins \
  --create-namespace \
  --values values/additional-plugins.yaml \
  --values values/admin-pass.yaml \
  --values values/configscripts-dark-theme.yaml \
  --values values/jenkins-url.yaml \
  --values values/configscripts-welcome-message.yaml \
  --values values/configscripts-versions-job.yaml \
  --values values/configscripts-disable-script-security.yaml

During execution, we observe in the casc_configs directory:

# Surveiller le répertoire casc_configs en temps réel
watch ls -la /var/jenkins_home/casc_configs/

The new files appear gradually:

  • dark-theme.yaml
  • welcome-message.yaml
  • versions-job.yaml
  • jcasc-default-config.yaml

And the Jenkins pod is recreated if necessary to incorporate all changes.


5.15 Final — Fully configured out of the box

The ultimate demonstration: completely uninstall Jenkins and reinstall it from scratch with the full deployment script.

# Lister les releases actuelles
helm list --all-namespaces

# Désinstaller l'instance jenkins1
helm uninstall jenkins1 --namespace jenkins

# Vérifier que tout est supprimé
helm list --all-namespaces
# → Aucune release

We reinstall with the deploy.sh script (which contains --install to create if nonexistent):

./helm/deploy.sh

During startup, we observe:

# Surveiller les pods et ConfigMaps
kubectl get pods,configmap -n jenkins --watch

We see:

  • ConfigMaps are created (including dark-theme, welcome-message, versions-job).
  • The pod starts (init containers, then main containers).

After reconnecting the port-forward and opening Jenkins:

  • Dark theme is active.
  • ✅ The welcome message is displayed as a banner.
  • ✅ The job versions is present in the dashboard.
  • ✅ By clicking Build Now for versions, an agent pod is created and the build runs successfully.
  • ✅ Everything works from the very first installation, without any manual action.

“If that’s not impressive, I don’t know what is!” —Wes Higbee

With this, we have reached the end of the course. The combination of Jenkins + Kubernetes + Helm + JCasC provides a robust, repeatable, and elastic CI/CD solution ideal for modern teams.


6. Course Resources and Repositories

GitHub Repositories

Tools used in the course

ToolRole
minikubeLocal Kubernetes cluster for development
kubectlKubernetes CLI to manage resources
helmKubernetes Package Manager
JenkinsCI/CD server (official image jenkins/jenkins:lts)
Kubernetes PluginJenkins ↔ Kubernetes integration for dynamic agents
JCasC pluginConfiguration as Code for Jenkins
DSL Job PluginAutomatic creation of Jenkins jobs by code
Eclipse TemurinOpen-source JDK distribution used for Java builds
MavenJava Build Tool (Maven Wrapper ./mvnw in project)

Quick Reference Commands

# ── minikube ─────────────────────────────────────────────────────────────────
minikube start --nodes=1 --profile=<nom>
minikube profile <nom>
minikube profile list
minikube stop --profile=<nom>
minikube ssh

# ── kubectl ──────────────────────────────────────────────────────────────────
kubectl config get-contexts
kubectl config set-context --current --namespace=jenkins
kubectl get all -n jenkins
kubectl get pv,pvc,sc -n jenkins
kubectl apply -f <fichier.yaml>
kubectl delete -f <fichier.yaml>
kubectl delete pod <nom-pod>
kubectl delete pvc <nom-pvc>
kubectl delete pv <nom-pv>
kubectl logs pod/<nom-pod> [-c <container>]
kubectl logs -l <label-selector> --follow
kubectl exec -it <nom-pod> [-c <container>] -- bash
kubectl port-forward pod/<nom-pod> 8080:8080
kubectl get events -n jenkins
kubectl describe pod <nom-pod> -n jenkins
kubectl rollout restart statefulset/<nom> -n jenkins

# ── helm ─────────────────────────────────────────────────────────────────────
helm repo add jenkins https://charts.jenkins.io
helm repo update
helm repo list
helm search repo jenkins
helm install <release> jenkins/jenkins --namespace <ns> --create-namespace
helm upgrade --install <release> jenkins/jenkins --namespace <ns> -f <values.yaml>
helm uninstall <release> --namespace <ns>
helm list --all-namespaces
helm get notes <release> --namespace <ns>
helm template <release> jenkins/jenkins --namespace <ns>

Final Architecture — Summary

┌─────────────────────────────────────────────────────────────────────┐
│  Cluster Kubernetes (minikube)                                       │
│                                                                      │
│  ┌─────────────── Namespace: jenkins ────────────────────────────┐  │
│  │                                                               │  │
│  │  ┌──────────────────────────────────────────────────────┐    │  │
│  │  │  Pod: jenkins1-0 (StatefulSet)                       │    │  │
│  │  │  ┌─────────────┐  ┌──────────────────────────┐      │    │  │
│  │  │  │  jenkins    │  │  config-reload (sidecar) │      │    │  │
│  │  │  │  (controller│  │  Surveille les ConfigMaps │      │    │  │
│  │  │  │   Jenkins)  │  │  → casc_configs/         │      │    │  │
│  │  │  └─────────────┘  └──────────────────────────┘      │    │  │
│  │  │         ↓ jenkins_home                               │    │  │
│  │  │  ┌─────────────┐                                     │    │  │
│  │  │  │  PVC/PV     │ (StorageClass: retained)            │    │  │
│  │  │  └─────────────┘                                     │    │  │
│  │  └──────────────────────────────────────────────────────┘    │  │
│  │                                                               │  │
│  │  Service: jenkins1         (port 8080)  ← UI + API           │  │
│  │  Service: jenkins1-agent   (port 50000) ← Agents JNLP        │  │
│  │                                                               │  │
│  │  ConfigMap: jenkins1             → plugins.txt, apply.sh     │  │
│  │  ConfigMap: jenkins1-jcasc-*     → configuration JCasC       │  │
│  │                                                               │  │
│  │  ┌──────────────────────────────────────────────────────┐    │  │
│  │  │  Pod Agent (éphémère, créé à la demande)             │    │  │
│  │  │  ┌────────┐  ┌────────┐  ┌────────┐                 │    │  │
│  │  │  │  jnlp  │  │ maven  │  │  jdk   │  ...            │    │  │
│  │  │  └────────┘  └────────┘  └────────┘                 │    │  │
│  │  │         ↓ workspace partagé entre containers         │    │  │
│  │  │  ┌─────────────┐                                     │    │  │
│  │  │  │  emptyDir   │ (PVC optionnel: shared-maven-repo)  │    │  │
│  │  │  └─────────────┘                                     │    │  │
│  │  └──────────────────────────────────────────────────────┘    │  │
│  │         ↑ créé par Jenkins via l'API Kubernetes               │  │
│  │         ↑ supprimé à la fin du build                          │  │
│  └───────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘


Search Terms

running · jenkins · kubernetes · ci/cd · git · devops · pod · container · namespace · pods · service · statefulset · agent · connect · containers · controller · helm · jcasc · parallel · pipeline · plugins · run · template · via

Interested in this course?

Contact us to book it or get a custom training plan for your team.