Table of Contents
- 2.1 Starting minikube
- 2.2 Jenkins Namespace Creation
- 2.3 Jenkins via a StatefulSet
- 2.4 Start and connect to Jenkins
- 2.5 PersistentVolume provisioning for /var/jenkins_home
- 2.6 InitialAdminPassword via HostPath directory
- 2.7 What happens if the Pod fails?
- 2.8 Default Reclaim Policy: Delete
- 2.9 Add a “retained” StorageClass
- 2.10 Disable Executors on Controller
- 2.11 Destroy everything and lose nothing
- 3.1 Kubernetes plugin installation
- 3.2 Grant access to the Kubernetes API
- 3.3 Create a Pod Template
- 3.4 Connect Agent Pod to Port 8080
- 3.5 Connect Agent Pod to Port 50000
- 3.6 Observe the Life Cycle of a Pod
- 3.7 Trigger 3 Concurrent Builds
- 3.8 Delay Pod Termination
- 3.9 Reuse of Pods for multiple Builds
- 4.1 Custom Pod Template in a Declarative Pipeline
- 4.2 Add a Maven Container
- 4.3 container() — Execute mvn in Maven Container
- 4.4 podTemplate() in a Scripted Pipeline
- 4.5 Checkout of a Git repository in the Pod Workspace
- 4.6 A JDK Container with the Eclipse Temurin image
- 4.7 Exec in Pod to inspect Workspace
- 4.8 Run Capture step
- 4.9 DSLs: containerTemplate() + persistentVolumeClaim()
- 4.10 Pod waiting until PVC created
- 4.11 Multiple Containers in Parallel in a Pod
- 4.12 Multiple Pods in Parallel
- 4.13 Eliminate Stuck Pods with terminationGracePeriodSeconds
- 5.1 Switch to a new Minikube Cluster
- 5.2 Add Jenkins repository to Helm
- 5.3 Reinstall in Jenkins Namespace
- 5.4 Install a Second Jenkins Instance
- 5.5 Kubernetes Pod Agents available out of the box
- 5.6 Examine Created Resources
- 5.7 ConfigMaps define how to start Jenkins
- 5.8 Specify additional plugins
- 5.9 Update Chart Values to add Plugins
- 5.10 Change Admin Password
- 5.11 Apply Dark Theme via JCasC
- 5.12 The /var/jenkins_home/casc_configs directory
- 5.13 The Jenkins Multi-Container Pod Controller
- 5.14 Initiating a Job Pipeline (Seeding)
- 5.15 Final — Fully configured on first install
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
- Deploy Jenkins on a Kubernetes cluster.
- Run agent pods on demand, without pre-provisioning.
- Customize these pods according to the specific needs of each pipeline (tools, versions, Docker images).
- 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:
- Start minikube with a profile named
manifest(this name corresponds to the first cluster of the course, used for deployment by manifest). - 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_homedirectory. - 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
volumeClaimTemplatessection automatically creates a PersistentVolumeClaim (PVC) namedjenkins-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:
- The StatefulSet is created.
- A pod
jenkins-0appears with statusContainerCreating(the image is being downloaded). - 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:
- Pod
jenkins-0entersTerminatingstate. - It is immediately recreated with the same name
jenkins-0. - 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-0also 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:
- Go to the Jenkins dashboard.
- Create a new job of type Pipeline named
hello. - In the pipeline configuration, choose the
Hello Worldscript (or paste a preconfigured script). - 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:
- Go to Manage Jenkins → System Configuration (or Configure System).
- Find the parameter Number of executors.
- Change it to
0. - 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.
- Go to Manage Jenkins → Plugins.
- Click on Available plugins.
- Search for
Kubernetes. - Select the plugin simply named Kubernetes (not “Kubernetes CLI” or other variant).
- 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
- In Manage Jenkins → Clouds, create a new cloud of type Kubernetes.
- Expand Cloud details.
- 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:
- Return to Manage Jenkins → Clouds → configure the Kubernetes cloud.
- Click Test Connection again. This time the connection is successful.
- Click on Save.
- Reopen the configuration and go to Pod templates.
- Click on Add a pod template.
- Configure:
- Name:
test - Namespace:
jenkins - Usage:
Use this node as much as possible
- 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 test → Configure:
- 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 templates → Configure 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:
- The pod is created (status
Pending). - It goes to
Running. - The build runs (~10 seconds for the hello pipeline).
- It goes to
Terminating. - 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:
- Scroll to the Pod Retention section.
- Find the parameter Time in minutes to retain agent when idle (or similar).
- Set it to
1(one minute). - 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:
- Trigger a build, wait for it to complete.
- 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:
- Select the agent directive.
- Choose type Kubernetes.
- 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.
- 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) andjnlp(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). Withoutnode(), there is no workspace: impossible to retrieve code or produce files.POD_LABELis a variable automatically set by the Kubernetes plugin to designate the label of the pod created by the enclosingpodTemplate.
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-modeparameter 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-6corresponds to build number 6 of jobscripted.
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
recordCoveragestep requires the Coverage plugin (formerly JaCoCo Plugin). Without this plugin, the build fails with the errorNo 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
.jarfile. - 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:
- Pure YAML: Define a
volumeMountand avolumeof typepersistentVolumeClaimdirectly in the YAML of the pod spec. - DSL Jenkins: Use the
containerTemplate()andpersistentVolumeClaim()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). ThepersistentVolumeClaim()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 theshared-maven-repodirectory 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:
- The mixture of the two styles of pod configuration (YAML +
containerTemplate). - Running parallel branches in a single pod with multiple containers.
- 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
terminationGracePeriodSecondsgives him time to finish cleanly before being forcibly killed.
Propagating to nested pods: The definition of
terminationGracePeriodSecondsin thepodTemplateofbuildalso applies to thenestedpod, 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:
- In instance 1 (port 8080): create a
test-pipelineitem. - In instance 2 (port 8090): check that
test-pipelinedoes 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:
- In the test instance, configure a pipeline with custom containers (copy one of the pipelines from module 4).
- 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
jenkinsUrlandjenkinsTunnel. - ✅ Default Pod template (with
jnlpcontainer) 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).
- ServiceAccount →
jenkins1(specific to this release, notdefault). - 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
jenkins1on port 8080). - The agent tunnel (service
jenkins1-agenton port 50000). - A default pod template with the
jnlpcontainer.
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.additionalPluginsparameter 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 Jenkins → Installed 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
--installargument tohelm upgrade --installcreates 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:
- Go to Manage Jenkins → Configuration as Code.
- 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:
- Manage Jenkins → Configuration as Code → Reload existing configuration.
- Search for
appearancein the displayed configuration. - We see
themeManagerwiththeme: darkanddisableUserThemes: 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)
config-reload-init: Performs a first load of the JCasC ConfigMaps in thecasc_configsdirectory before Jenkins starts.
Regular Containers (run in parallel)
jenkins: The main Jenkins controller.
- Mount:
/var/jenkins_home→ PVC. - Mount:
/var/jenkins_home/casc_configs→ JCasC ConfigMaps.
config-reload: A sidecar container which:
- Continuously monitors changes in ConfigMaps with a specific label.
- Copy changes to
casc_configsdirectory. - 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.yamlwelcome-message.yamlversions-job.yamljcasc-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
versionsis 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
-
Main course repository: https://github.com/g0t4/course-jenkins-k8s Contains:
-
Cluster startup scripts (
cluster/). -
YAML manifests (
manifest/). -
Helm values files (
helm/values/). -
Example Groovy pipelines (
pipelines/). -
Demo Maven Project (Modified Spring PetClinic): https://github.com/g0t4/course-jenkins-k8s-spc Used in Module 4 demos to demonstrate a real Maven build with testing and code coverage.
Tools used in the course
| Tool | Role |
|---|---|
minikube | Local Kubernetes cluster for development |
kubectl | Kubernetes CLI to manage resources |
helm | Kubernetes Package Manager |
| Jenkins | CI/CD server (official image jenkins/jenkins:lts) |
| Kubernetes Plugin | Jenkins ↔ Kubernetes integration for dynamic agents |
| JCasC plugin | Configuration as Code for Jenkins |
| DSL Job Plugin | Automatic creation of Jenkins jobs by code |
| Eclipse Temurin | Open-source JDK distribution used for Java builds |
| Maven | Java 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