# DevNet Lab 12 -- Build a Sample Web App CI/CD Pipeline Using Jenkins
[toc]
---
### Scenario
In this lab, you will take the sample application code from the previous lab [(Lab 11 – Build a Sample Web App in a Podman Container)](https://md.inetdoc.net/s/xNCuzwbfX) and commit it to a new Git repository. You will install and configure Jenkins, then use it to automate the downloading and running of your sample application. Next, you’ll create a Jenkins test job to verify that the application runs correctly with each build. Finally, you will integrate both the build and test jobs into a continuous integration/continuous delivery (CI/CD) pipeline, ensuring that the application is always ready for deployment whenever code changes occur

The diagram below illustrates how identities interact within the lab. The default VM admin account, named `etu` prepares both systems. The Jenkins service account manages pipeline tasks, while the Jenkins agent account runs these tasks on the worker VM. The Jenkins agent owns the Podman containers that are built from these tasks. GitLab manages the Git repositories, to which the Jenkins service account has secure access via SSH keys.
```mermaid
flowchart LR
%% Styles for *VM blocks* only
classDef gitlab fill:#ffa500,stroke:#333,stroke-width:1px,color:#000;
classDef devnet fill:#a2f3a2,stroke:#333,stroke-width:1px,color:#000;
classDef worker fill:#a2c5f3,stroke:#333,stroke-width:1px,color:#000;
%% GitLab
subgraph GITLAB["gitlab.inetdoc.net"]
direction TB
GITLAB_SERVICE["GitLab service\nStudents Git repositories"]
end
class GITLAB gitlab
%% Do NOT apply gitlab style to GITLAB_SERVICE to keep this block white
%% DevNet VM
subgraph DEVNET_VM["DevNet VM"]
direction TB
ETU_DEV["etu\nDevNet system admin"]
JENKINS_DEV["jenkins\nJenkins Service account"]
end
class DEVNET_VM devnet
%% Do NOT apply devnet style to ETU_DEV,JENKINS_DEV to keep them white
%% worker VM
subgraph WORKER_VM["worker VM"]
direction TB
ETU_WORKER["etu\nWorker system admin"]
JENKINS_AGENT["jenkins-agent\nJenkins agent account\nPodman containers owner"]
end
class WORKER_VM worker
%% Do NOT apply worker style to ETU_WORKER,JENKINS_AGENT
%% Local delegation / configuration
ETU_DEV -->|"sudo / Jenkins config\npackage install,\nSSH keys config"| JENKINS_DEV
ETU_WORKER -->|"sudo / jenkins-agent config\naccount creation,\nPodman config"| JENKINS_AGENT
%% Jenkins -> worker (job execution)
JENKINS_DEV -->|"SSH with jenkins key\nruns jobs,\ncontrols jenkins-agent"| JENKINS_AGENT
%% GitLab access
GITLAB_SERVICE <-->|"etu SSH key\ninitial clone/push"| ETU_DEV
GITLAB_SERVICE -->|"jenkins SSH key\npull/clone Jenkins jobs"| JENKINS_DEV
```
### Objectives
Upon completing the lab activities, students will be able to:
- Commit the sample application code to a new Git repository and ensure proper version control is set up.
- Install and configure Jenkins, including setting up secure communication with a worker node for automated builds.
- Create Jenkins jobs to build and test the sample application within a Podman container and verify its successful execution.
- Integrate the build and test jobs into a Jenkins pipeline to automate the application's CI/CD processes.
## Part 1: Copy and Commit the sample app code to Git
In this part, you will create a GitLab repository to commit the sample app files you created in the previous lab.
### Step 1: Log in to GitLab and create a new repository
- Log in at https://gitlab.inetdoc.net/ with your credentials
- Select the **Groups** item on the left panel menu
- Select your personal group named with your username and then the **New Project** button in the upper right corner of the window.
- Create a new blank project with a name of your choice - **Lab12** seems like an easy choice.
### Step 2: Check your Git configuration settings in the DevNet VM
Since Git was configured in a previous lab ([DevNet Lab 6 – Software Version Control with Git](https://md.inetdoc.net/s/hOTo4nKku)), your Git configuration parameters should already be set. This step is a simple parameter check.
```bash
git config --list --global
```
```bash=
user.name=Etudiant Test
user.email=etuXXXXXX@example.com
init.defaultbranch=main
pull.rebase=false
```
If any parameters are missing, you must fix them now. Here is a list of instructions:
```bash
git config --global user.name "Sample User"
git config --global user.email etuXXXXXX@example.com
git config --global init.defaultBranch main
git config --global pull.rebase false
```
Finally, we also check SSH key authentication to the GitLab service.
```bash
ssh -T git@gitlab.inetdoc.net
```
```bash=
Welcome to GitLab, @etuXXXXXX!
```
### Step 3: Clone the lab Git repository
Assuming all your lab files are stored in subdirectories of `$HOME/labs`, you simply need to clone the new Git repository to that location in your personal tree.
From the GitLab project window, select the **Code** blue button and then the **Clone with SSH** URL to copy your own repository address.
```bash
cd $HOME/labs
git clone git@gitlab.inetdoc.net:etuXXXXXX/lab12.git
```
```bash=
Cloning into 'lab12'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (3/3), done.
```
You are now ready to open this new folder in Visual Studio Code on the DevNet VM..
### Step 4: Stage, commit, and push the sample app files to the GitLab repository
Start by copying the previous lab `sample-app` directory. Then add this directory to the git repository after removing the `tempdir` directory that was used for temporary files.
```bash
cp -ar ../lab11/sample-app/* .
```
```bash=
tree -L 2
.
├── README.md
├── sample-app.py
├── sample-app.sh
├── static
│ └── style.css
├── templates
└── index.html
```
You are ready to add the `sample-app` directory in the local Git repository.
```bash
git add .
git status
```
```bash=
On branch main
Your branch is up to date with 'origin/main'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: README.md
new file: .gitignore
new file: sample-app.py
new file: sample-app.sh
new file: static/style.css
new file: templates/index.html
```
The sample app code is now ready to be staged.
```bash
git commit -am "Committing sample-app files to Lab12 repo"
```
```bash=
[main 50fc211] Committing sample-app files to Lab12 repo
6 files changed, 61 insertions(+), 92 deletions(-)
create mode 100644 .gitignore
create mode 100644 sample-app.py
create mode 100644 sample-app.sh
create mode 100644 static/style.css
create mode 100644 templates/index.html
```
Finally, we can push the staged files to the GitLab service repository and move on to the next part.
```bash
git push origin main
```
```bash=
Enumerating objects: 13, done.
Counting objects: 100% (13/13), done.
Delta compression using up to 8 threads
Compressing objects: 100% (7/7), done.
Writing objects: 100% (11/11), 1.31 KiB | 1.31 MiB/s, done.
Total 11 (delta 0), reused 0 (delta 0), pack-reused 0
To gitlab.inetdoc.net:etuXXXXXX/lab12.git
c63f8d6..50fc211 main -> main
```
## Part 2: Install and run the Jenkins service on the DevNet VM
In this part, you will install the Jenkins service on the DevNet VM and configure the Jenkins user account.
:::info
The Jenkins service and the worker instances communicate via SSH. Beyond installing a package, you must configure two user identities with passwordless SSH authentication from the Jenkins service to the worker node.
:::
### Step 1: Install the Jenkins service on the DevNet VM
Since the Jenkins service relies on Java, we need to check the version of Java installed via the **default-jdk-headless** package. This package should already be installed in the DevNet VM.
```bash
java -version
```
```bash=
openjdk 21.0.10 2026-01-20
OpenJDK Runtime Environment (build 21.0.10+7-Debian-1)
OpenJDK 64-Bit Server VM (build 21.0.10+7-Debian-1, mixed mode, sharing)
```
Once the Java installation is verified, you can move on to package management by adding a new repository key and a new package source definition before installing the jenkins package itself.
1. Add the Jenkins repository key
```bash
curl -fsSL https://pkg.jenkins.io/debian/jenkins.io-2026.key |\
sudo tee /usr/share/keyrings/jenkins-keyring.asc > /dev/null
```
2. Add the Jenkins package source list
```bash
echo deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] \
https://pkg.jenkins.io/debian binary/ | sudo tee \
/etc/apt/sources.list.d/jenkins.list > /dev/null
```
3. Install the jenkins package
```bash
sudo apt update && sudo apt -y install jenkins
```
4. Identify the properties of the `jenkins` user account created during package installation
```bash
getent passwd jenkins
```
```bash=
jenkins:x:103:104:Jenkins:/var/lib/jenkins:/bin/bash
```
### Step 2: Prepare the `jenkins` user account for SSH communication with the worker node
Start by assigning a password to the DevNet VM `jenkins` user account:
```bash
sudo su - -c "passwd jenkins"
```
```bash=
New password:
Retype new password:
passwd: password updated successfully
```
Then we test the new password assignment:
```bash
su - jenkins
```
```bash=
Password:
jenkins@devnet:~$
```
```bash
pwd
```
```bash=
/var/lib/jenkins
```
```bash
ls -l
```
```bash=
ls -l
total 44
-rw-r--r-- 1 jenkins jenkins 1579 6 févr. 17:53 config.xml
-rw-r--r-- 1 jenkins jenkins 156 6 févr. 17:53 hudson.model.UpdateCenter.xml
-rw-r--r-- 1 jenkins jenkins 171 6 févr. 17:53 jenkins.telemetry.Correlator.xml
drwxr-xr-x 2 jenkins jenkins 4096 6 févr. 17:53 jobs
-rw-r--r-- 1 jenkins jenkins 1037 6 févr. 17:53 nodeMonitors.xml
drwxr-xr-x 2 jenkins jenkins 4096 6 févr. 17:53 plugins
-rw-r--r-- 1 jenkins jenkins 64 6 févr. 17:53 secret.key
-rw-r--r-- 1 jenkins jenkins 0 6 févr. 17:53 secret.key.not-so-secret
drwx------ 2 jenkins jenkins 4096 6 févr. 17:53 secrets
drwxr-xr-x 2 jenkins jenkins 4096 6 févr. 17:53 updates
drwxr-xr-x 2 jenkins jenkins 4096 6 févr. 17:53 userContent
drwxr-xr-x 3 jenkins jenkins 4096 6 févr. 17:53 users
```
:::info
Notice the presence of a directory called `secrets` which contains a file named **initialAdminPassword** that we will use for the first web service authentication.
:::
```bash
ls -l secrets
```
```bash=
total 16
-rw-r--r-- 1 jenkins jenkins 48 6 févr. 17:53 hudson.model.User.DIRNAMES
-rw-r----- 1 jenkins jenkins 33 6 févr. 17:53 initialAdminPassword
-rw-r--r-- 1 jenkins jenkins 32 6 févr. 17:53 jenkins.model.Jenkins.crumbSalt
-rw-r--r-- 1 jenkins jenkins 256 6 févr. 17:53 master.key
```
Using this `jenkins` user account, you are ready to run a one-line command to prepare for passwordless SSH authentication to the Jenkins worker on the container server host.
```bash
ssh-keygen -q -t ed25519 -C 'Jenkins service' -N '' -f $HOME/.ssh/id_ed25519
```
To verify that the command was successful, look at the contents of the `.ssh` directory.
```bash
ls -l .ssh/
```
```bash=
total 8
-rw------- 1 jenkins jenkins 411 avril 2 10:46 id_ed25519
-rw-r--r-- 1 jenkins jenkins 97 avril 2 10:46 id_ed25519.pub
```
The SSH key pair is ready to be transferred to the worker node, but first we need to create the `jenkins-agent` user on that target host.
## Part 3: Start the worker VM and create the `jenkins-agent` user account
Starting a Jenkins worker virtual machine to launch Podman containers requires one mandatory configuration step: creating a `jenkins-agent` user account with the following properties:
* Accessible via a passwordless SSH connection from the DevNet `jenkins` service user account.
* Capable of building and launching Podman containers.
### Step 1: Start the worker virtual machine
If it has not already been created, you must first start the worker VM to add the new `jenkins-agent` user account.
Therefore, we need to create two YAML definition files, one for the network connection and one for the VM itself.
1. Here is a sample network connection declaration file, which you can name `lab12-switch.yaml`:
```yaml=
ovs:
switches:
- name: dsw-host
ports:
- name: tapYYY # <- YOUR OWN TAP INTERFACE NUMBER
type: OVSPort
vlan_mode: access
tag: OOB_ID # <- YOUR OUT-OF-BAND VLAN ID
```
2. Here is a second sample VM declaration file, which can be named `lab12-worker.yaml`:
```yaml=
kvm:
vms:
- vm_name: lab12-worker
os: linux
master_image: debian-testing-amd64.qcow2
force_copy: false
memory: 4096
tapnum: YYY # <- YOUR OWN TAP INTERFACE NUMBER
cloud_init:
force_seed: false
hostname: worker
packages:
- default-jdk-headless
write_files:
- path: /etc/sudoers.d/etu
permissions: '0440'
owner: root:root
content: |
# etu is an administrative user account
etu ALL=(ALL:ALL) NOPASSWD: ALL
```
:::warning
Do not forget to replace the YYY placeholder with your own virtual machine TAP interface number.
:::
After creating these two files on the hypervisor, use the hypervisor terminal to run the two scripts and start the worker VM.
```bash
$HOME/masters/scripts/switch-conf.py --apply lab12-switch.yaml
$HOME/masters/scripts/lab-startup.py lab12-worker.yaml
```
Wait for the virtual machine to start, then retrieve its list of IPv4 and IPv6 addresses.
```bash
my-vms.py ls --running --name lab12-worker --json |\
jq -r '.virtual_machines[].ipv6_addresses[].address'
```
```bash=
fe80::baad:caff:fefe:YYY
2001:678:3fc:VVVV:baad:caff:fefe:YYY
```
Once you have the addresses of the worker virtual machine, add a new entry to the DevNet VM SSH client configuration file.
```bash=
cat << EOF >>$HOME/.ssh/config
Host workerYYY # <-- REPLACE YYY BY YOUR WORKER TAP INTERFACE NUMBER
HostName fe80::baad:caff:fefe:YYY%%enp0s1
User etu
Port 22
EOF
```
Use the `ssh-copy-id` command to copy the DevNet user's SSH public key to the worker virtual machine.
```bash
ssh-copy-id -o StrictHostKeyChecking=accept-new workerYYY
```
```bash=
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: ssh-add -L
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
(etu@fe80::baad:caff:fefe:2%enp0s1) Password:
Number of key(s) added: 1
Now try logging into the machine, with: "ssh -o 'StrictHostKeyChecking=accept-new' 'worker'"
and check to make sure that only the key(s) you wanted were added.
```
Finally, we will test the passwordless SSH connection to this worker node and verify that the **`default-jdk-headless`** package is installed. This package is required for the worker virtual machine to become a Jenkins service node.
```bash
ssh workerYYY "sudo apt list --installed default-jdk-headless"
```
```bash=
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
En train de lister…
default-jdk-headless/testing,now 2:1.21-76 amd64 [installé]
```
### Step 2: Create the `jenkins-agent` user account and install Podman remotely
From the DevNet VM, you run a locally developed Bash script over an SSH connection.
Here is a script whose purpose is to create a new `jenkins-agent` account with the ability to create and run Podman containers.
```bash=
#!/usr/bin/env bash
# Create and configure a Jenkins agent user account on the worker VM
# Run this script as follows:
# ssh workerYYY 'sudo bash -s' < setup-jenkins-agent.sh
set -euo pipefail
# Check if the script is run as root
if [[ ${EUID} -ne 0 ]]; then
echo "This script must be run as root." >&2
exit 1
fi
# Username for the Jenkins agent
readonly USERNAME="jenkins-agent"
# Jenkins agent password generation if needed
PASSWD_FILE="${HOME}/.jenkins-agent.passwd"
if [[ ! -f ${PASSWD_FILE} ]]; then
echo "Generating random password..."
openssl rand -base64 24 >"${PASSWD_FILE}"
chmod 600 "${PASSWD_FILE}"
else
echo "Password file already exists. Using existing password."
fi
# Read the password from the file
PASSWORD=$(cat "${PASSWD_FILE}")
if [[ -z ${PASSWORD} ]]; then
echo "ERROR: Failed to read password from ${PASSWD_FILE}" >&2
exit 1
fi
readonly PASSWORD
# Create the user
if id "${USERNAME}" &>/dev/null; then
echo "User ${USERNAME} already exists"
else
echo "Creating user ${USERNAME}"
adduser --disabled-password --gecos "" "${USERNAME}"
echo "${USERNAME}:${PASSWORD}" | chpasswd
fi
# Install podman if not already installed
if ! command -v podman &>/dev/null; then
apt update
apt install -y podman
else
echo "Podman is already installed"
fi
# Enable the Jenkins agent user to run Podman
SUBCHANGE=false
if grep -q "${USERNAME}" /etc/subuid; then
echo "User ${USERNAME} already has subuid entry"
else
echo "${USERNAME}":165536:65536 | tee -a /etc/subuid
SUBCHANGE=true
fi
if grep -q "${USERNAME}" /etc/subgid; then
echo "User ${USERNAME} already has subgid entry"
else
echo "${USERNAME}":165536:65536 | tee -a /etc/subgid
SUBCHANGE=true
fi
if [[ ${SUBCHANGE} == true ]]; then
echo "Subuid and subgid entries added for user ${USERNAME}"
podman system migrate
fi
# Check whether linger is enabled for the Jenkins agent user.
if loginctl show-user "${USERNAME}" | grep -q "Linger=yes"; then
echo "Linger is already enabled for user ${USERNAME}"
else
echo "Enabling linger for user ${USERNAME}"
loginctl enable-linger "${USERNAME}"
fi
exit 0
```
Once the script code is stored in the `setup-jenkins-agent.sh` file, you can run the following command to run it remotely.
```bash
ssh workerYYY 'sudo bash -s' <setup-jenkins-agent.sh
```
### Step 3: Configure passwordless SSH connections from the Jenkins service to the agent on the worker VM
Start by running commands to retrieve the jenkins-agent user password from the worker. This is required to enable seamless, passwordless communication between the Jenkins service on the DevNet VM and its agent on the worker.
```bash
ssh workerYYY "sudo cat /root/.jenkins-agent.passwd" |\
sudo bash -c 'cat > /var/lib/jenkins/secrets/worker.jenkins-agent.passwd'
sudo chown jenkins:jenkins /var/lib/jenkins/secrets/worker.jenkins-agent.passwd
sudo chmod 400 /var/lib/jenkins/secrets/worker.jenkins-agent.passwd
```
This secret transfer allows us to open an SSH connection to the new `jenkins-agent` user account on the worker node.
Before initiating this new SSH connection, you must switch to the Jenkins service identity by opening a session with the `jenkins` user account on the DevNet VM.
```bash
su - jenkins
```
Once the `jenkins` user session is open, you need to repeat the SSH client configuration by adding a new entry for the worker node VM.
```bash
touch $HOME/.ssh/config
cat << EOF >>$HOME/.ssh/config
Host workerYYY # <-- REPLACE YYY BY YOUR WORKER TAP INTERFACE NUMBER
HostName fe80::baad:caff:fefe:YYY%%enp0s1
User jenkins-agent
Port 22
EOF
```
You are now ready to start a new SSH connection from the Jenkins service user account on the DevNet VM to the jenkins-agent account on the remote worker node. To avoid using the `sshpass` command, you must to copy the Jenkins service SSH public key to the worker node.
```bash
sshpass -f $HOME/secrets/worker.jenkins-agent.passwd \
ssh-copy-id -o StrictHostKeyChecking=accept-new workerYYY
```
```bash=
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/var/lib/jenkins/.ssh/id_ed25519.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
Number of key(s) added: 1
Now try logging into the machine, with: "ssh -o 'StrictHostKeyChecking=accept-new' 'worker'"
and check to make sure that only the key(s) you wanted were added.
```
```bash
ssh workerYYY "loginctl list-users"
```
```bash=
UID USER LINGER STATE
1002 jenkins-agent yes active
1 users listed.
```
At this point, the worker virtual machine is fully prepared to act as a Jenkins agent, with secure passwordless SSH access and Podman properly configured for container workloads.
## Part 4: Configure the Jenkins service
In this part, you will complete the initial configuration of the Jenkins service.
### Step 1: Open a web browser tab.
Use port forwarding from your personal machine to the DevNet VM hosting the Jenkins service to access the Jenkins web service directly from your browser.
The Jenkins web service listens on port 8080 on the DevNet VM. Therefore, you must forward the local port 8080 to the DevNet VM via the SSH connection. This can be done from the command line or from the SSH client configuration file.
* Confirm the service is listening on port 8080 on the Devnet VM:
```bash
sudo lsof -i tcp:8080
```
```bash=
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 875 jenkins 9u IPv6 13314 0t0 TCP *:http-alt (LISTEN)
```
* Enable port forwarding from the command line:
```bash
ssh -L 8080:localhost:8080 devnet
```
* Enable port forwarding by editing the `devnet` entry of the SSH client configuration file:
```bash
grep -A5 devnet .ssh/config
```
```bash=
Host devnet
HostName 2001:678:3fc:VVV:baad:caff:fefe:XXX
User etu
Port 2222
ForwardAgent yes
LocalForward 8080 localhost:8080
```
:::warning
You must change the virtual machine IPv6 address to match your own DevNet VM address.
:::
Once the SSH connection is open, navigate to `http://localhost:8080/` and log in with the administrator password you copied from the following file.
```bash
sudo cat /var/lib/jenkins/secrets/initialAdminPassword
```
### Step 2: Complete the initial configuration of Jenkins
1. Install the recommended Jenkins plugins.
Click Install suggested plugins and wait for Jenkins to download and install the plugins. You will see log messages in a window panel as the installation progresses.
2. Create a new admin user.
When the installation is complete, you will be presented with the **Create First Admin User** window. Fill in the form and confirm the creation of the new Admin user.
3. Confirm the value of the Jenkins service URL.
For this lab, we will use `http://localhost:8080/` as the service URL.
4. Start using Jenkins
Click **Start Using Jenkins** in the next window. You should now be on the main dashboard with a Welcome to Jenkins! message.
### Step 3: Create SSH access credentials for the worker node
1. Go to **Dashboard > Manage Jenkins > Credentials > System > Global Credentials > Add Credentials**.

2. Select SSH Username with Private Key.
3. Username: jenkins-agent
4. Private Key: Paste the master’s private key (/var/lib/jenkins/.ssh/id_ed25519)
### Step 4: Add Agent Node
1. Navigate to **Dashboard > Manage Jenkins > Nodes > New Node**.
2. Configure settings:
- Name: **debian-agent**
- Remote root directory: **/home/jenkins-agent**
- Labels: **linux** (for job targeting)
- Launch method: **Launch agents via SSH**
- Host: **workerYYY** (Same name or address as in [Step 3: Configure passwordless SSH connections from the Jenkins service to the agent on the worker VM](#Step-3-Configure-passwordless-SSH-connections-from-the-Jenkins-service-to-the-agent-on-the-worker-VM))
- Host Key Verification Strategy: **Manually trusted Verification Strategy**
- Credentials: **Select the SSH key created earlier**
:::warning
Selecting the **Manually trusted verification strategy** option stores the host key from the first successful SSH connection. This means that no host key for this worker node must already be present in the `$HOME/.ssh/known_hosts` file of the jenkins user.
If an SSH connection from the jenkins user on the DevNet VM to the jenkins-agent user on the worker node has already been established, the `$HOME/.ssh/known_hosts` file already contains a (hashed) host key entry for this worker node.
Before relaunching the agent from Jenkins, you must delete the `$HOME/.ssh/known_hosts` file in the jenkins user account on the DevNet virtual machine.
```bash
su - jenkins # Use the jenkins user identity
rm -f $HOME/.ssh/known_hosts # Remove all known host keys
```
:::
After adding the node agent is complete, the node status information should look like the screenshot below.

You can also check the logs for messages proving that communication has been established between the Jenkins service and its agent. Here is an example:
```bash
<===[JENKINS REMOTING CAPACITY]===>channel started
Remoting version: 3355.v388858a_47b_33
Launcher: SSHLauncher
Communication Protocol: Standard in/out
Ceci est un agent Unix
Agent successfully connected and online
```
## Part 5: Use Jenkins to run a build of your application
The basic unit of Jenkins is the job. You can create jobs that perform a variety of tasks, including the following:
* Fetch code from a source code management repository such as GitLab.
* Build an application using a script or build tool.
* Package an application and run it on a server.
In this part, you will create a simple Jenkins job that fetches the latest version of your sample application from GitLab and runs a build script.
### Step 1: Give the `jenkins` user account access to your lab's Git repository
Before we dive into configuring the Jenkins build job, we need to make sure that the user account running the Jenkins web service has access to your lab's Git repository. To do this, we need to add the SSH public key for the `jenkins` user to your GitLab account.
1. Make a copy of the SSH public key for the `jenkins` user.
```bash
cat $HOME/.ssh/id_ed25519.pub
```
```bash=
ssh-ed25519 AAAA... Jenkins service
```
2. Go to the GitLab web user profile page and add a new SSH key.

3. Make sure the jenkins user has access to the Git repository.
```bash
ssh -o StrictHostKeyChecking=accept-new -T git@gitlab.inetdoc.net
```
```bash=
Warning: Permanently added 'gitlab.inetdoc.net' (ED25519) to the list of known hosts.
Welcome to GitLab, @student_name!
```
### Step 2: Create a new job
1. Click the **Create a Job** link just below the Welcome to Jenkins! message. Alternatively, you can click New Item from the menu on the left.
2. In the Enter an item name box, type **Build_Sample_App_Job**.
3. Click **Freestyle Project** for the Job Type. In the description, SCM stands for Software Configuration Management, a classification of software that is responsible for tracking and changing software.
4. Scroll down and click **OK**.
### Step 3: Configure the Jenkins Build_Sample_App_Job
We are now in the Job Configuration window, where you can enter details about your job. The tabs at the top left are just shortcuts to the sections below. Click through the tabs to explore the options you can configure. For this simple job, you only need to add a few configuration details.
1. In the General tab, add a description for your job. For example, "My sample application build job".
2. Select **Restrict where this project can be run** and enter **linux** as the **Label Expression**.

3. Go to the Source Control tab and select the Git radio button. In the Repository URL field, add your GitLab repository link for the sample application, making sure to include your case-sensitive username. Be sure to add the .git extension to the end of your URL. For example:
> git@gitlab.inetdoc.net:CohortXXXXX/SampleUser/ProjectName.git
4. For Credentials, click the **Add** button and select **Jenkins**.
In the Add Credentials dialog box, enter the following parameters:
* Description: Git Jenkins credentials (this avoids confusion with the Jenkins agent credentials).
* Username: jenkins
* Private key: paste the `jenkins` user private key copy made from the command:
```bash
cat $HOME/.ssh/id_ed25519
```
Remember to include the header and trailer of the `jenkins` user private key.
```bash
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
```

5. There is one last very important Git parameter: **Branch Specifier**
Make sure the Git branch name is **main** and not **master**.

6. Go to the **Build Steps** tab and click **Add build step**. Then, choose **Execute shell**. In the **Command** field, enter the command to run the `sample-app.sh` script.
```bash
bash ./sample-app.sh
```

That's it for the build job configuration! You can click on the Save button.
### Step 4: Let Jenkins build the application.
Click **Build Now** on the left to start the job. Jenkins will clone your Git repository and run the build command bash `./sample-app.sh`. Your build should be successful because you did not change anything in the code from the previous lab.
In the **Build History** section on the left, click on your build number, which should be #1 unless you built the app multiple times.
Still in the left panel, click **Console Output**. You should see output similar to the following logs copied below. Take a close look at the logs and note the important messages.
- **Building remotely on Debian Worker**: The job is built on the worker virtual machine with the jenkins-agent identity, as the workspace points to this user's home directory.
- All the Dockerfile steps are completed
- **Finished: SUCCESS**: The Podman container is built and *should be* running.
```bash=
Started by user jenkins Admin
Running as SYSTEM
Building remotely on Debian Worker (linux) in workspace /home/jenkins-agent/workspace/Build_Sample_App_Job
The recommended git tool is: NONE
using credential 0b331a3a-874c-40cb-892c-1dff9ede2621
> git rev-parse --resolve-git-dir /home/jenkins-agent/workspace/Build_Sample_App_Job/.git # timeout=10
Fetching changes from the remote Git repository
> git config remote.origin.url git@gitlab.inetdoc.net:devnet/lab12.git # timeout=10
Fetching upstream changes from git@gitlab.inetdoc.net:devnet/lab12.git
> git --version # timeout=10
> git --version # 'git version 2.47.2'
using GIT_SSH to set credentials Jenkins agent Git credentials
Verifying host key using known hosts file
> git fetch --tags --force --progress -- git@gitlab.inetdoc.net:devnet/lab12.git +refs/heads/*:refs/remotes/origin/* # timeout=10
> git rev-parse refs/remotes/origin/main^{commit} # timeout=10
Checking out Revision c6aa6d1fa0319b8d3f4e1a027a62a3d430e9e209 (refs/remotes/origin/main)
> git config core.sparsecheckout # timeout=10
> git checkout -f c6aa6d1fa0319b8d3f4e1a027a62a3d430e9e209 # timeout=10
Commit message: "Ajout du script de construction et d'exécution du conteneur sampleapp du lab11"
> git rev-list --no-walk be5284a52c5bf2189a73adf0536f903ae259075b # timeout=10
[Build_Sample_App_Job] $ /bin/sh -xe /var/tmp/jenkins12909392112732794951.sh
+ bash ./sample-app.sh
STEP 1/8: FROM docker.io/library/python
STEP 2/8: ENV PIP_ROOT_USER_ACTION=ignore
--> Using cache 1828b988e974c5e5ec408a946400e5f22df03fd75783343ac5f9b66710feeeab
--> 1828b988e974
STEP 3/8: RUN pip3 install --upgrade flask
--> Using cache 824f287c39a381cbef625dde8116e39130c9b6c7860949bc2ad1eed529cee9b2
--> 824f287c39a3
STEP 4/8: COPY ./static /home/myapp/static/
--> Using cache 6394a71c79786a09eda14a4aa07cb0d38e397bb8ce6991e232591fd08fd7676a
--> 6394a71c7978
STEP 5/8: COPY ./templates /home/myapp/templates/
--> Using cache 588709bf2fe56a9c5952ab620717202de1ac34bf5266490ef25b93234afe1c3f
--> 588709bf2fe5
STEP 6/8: COPY sample-app.py /home/myapp/
--> Using cache 8cd9f53f9262153c949830e3f8872df719b699d68228f4ffb76495b2f71ee7b6
--> 8cd9f53f9262
STEP 7/8: EXPOSE 8081
--> Using cache 1424b88e7e09d3746ae54ccc2bec92c9f89dd26f4e9982355090663f817ea248
--> 1424b88e7e09
STEP 8/8: CMD python3 /home/myapp/sample-app.py
--> Using cache 2f4bb12d6254f7325275ad728ff4795b258b15f368deb1ef6440c00b6d1af870
COMMIT sampleapp
--> 2f4bb12d6254
Successfully tagged localhost/sampleapp:latest
2f4bb12d6254f7325275ad728ff4795b258b15f368deb1ef6440c00b6d1af870
f5053bd0d06df5c98f7bae371e302e2bb25648f6103d484c0d27715e05a75e5d
Finished: SUCCESS
```
Now, let's check the state of the container on the worker virtual machine. Open an SSH connection to the VM and list the Podman processes.
```bash
su - jenkins
Mot de passe :
jenkins@devnet:~$ ssh worker
```
```bash
jenkins-agent@worker:~$ podman ps -a
```
```bash=
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f5053bd0d06d localhost/sampleapp:latest /bin/sh -c python... 3 minutes ago Exited (143) 3 minutes ago 0.0.0.0:8081->8081/tcp samplerunning
```
As Podman's process list shows, the status of the `samplerunning` container is "Exited", and the web application is not running. Although the build job finished successfully, the container stopped running.
:::warning
Investigating exit code 143 shows that the container started properly but was stopped at the end of the Jenkins job. This is a common issue when building containers remotely. Therefore, you need a more robust and reliable method for building our containers.
:::
### Step 5: Edit the Bash job script to make the application container startup reliable
Here is the new Bash job script, `sample-app.sh`, which adds the creation of a systemd Quadlet service compared with the previous version.
:::info
A **systemd Quadlet** service manages Podman containers and related resources as native systemd services through a simplified, declarative configuration file. By bridging Podman and systemd, Quadlets make it easier to run containers as systemd-managed background services with automatic startup, persistence, dependency management, logging, and health monitoring.
:::
```bash=
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# Ensure podman is properly configured for rootless operation
XDG_RUNTIME_DIR="/run/user/$(id -u)"
export XDG_RUNTIME_DIR
if ! command -v podman >/dev/null 2>&1; then
echo "podman is required but not installed." >&2
exit 1
fi
for path in sample-app.py templates static; do
if [[ ! -e ${path} ]]; then
echo "Missing required path: ${path}" >&2
exit 1
fi
done
tempdir="$(mktemp -d)"
trap 'rm -rf "${tempdir}"' EXIT
cp sample-app.py "${tempdir}/"
cp -r templates "${tempdir}/"
cp -r static "${tempdir}/"
cat <<EOF >"${tempdir}/Dockerfile"
FROM docker.io/library/python
ENV PIP_ROOT_USER_ACTION=ignore
RUN pip3 install --upgrade flask
COPY ./static /home/myapp/static/
COPY ./templates /home/myapp/templates/
COPY sample-app.py /home/myapp/
EXPOSE 8081
CMD python3 /home/myapp/sample-app.py
EOF
cd "${tempdir}"
# Build the container image
echo "Building container image..."
podman build -t sampleapp .
# Clean up any existing container with the same name
echo "Cleaning up existing container..."
podman rm -f samplerunning 2>/dev/null || true
# Ensure systemd user service directory exists
mkdir -p ~/.config/containers/systemd
# Create Quadlet file for modern systemd integration
echo "Creating Quadlet configuration..."
cat <<EOF >~/.config/containers/systemd/samplerunning.container
[Unit]
Description=Sample Flask Application Container
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers
[Container]
Image=localhost/sampleapp:latest
ContainerName=samplerunning
PublishPort=8081:8081
AutoUpdate=registry
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=default.target
EOF
# Reload systemd to pick up the new Quadlet
echo "Reloading systemd and starting service..."
systemctl --user daemon-reload
# systemd will automatically create the service from the Quadlet file
SERVICE_NAME="samplerunning"
echo "Starting Quadlet service..."
systemctl --user start "${SERVICE_NAME}"
# Verification that service started
sleep 3
if systemctl --user is-active "${SERVICE_NAME}" --quiet; then
echo "✓ Quadlet service started successfully"
RUNNING_CONTAINERS=$(podman ps --filter name=samplerunning --filter status=running --format "{{.Names}}")
if echo "${RUNNING_CONTAINERS}" | grep -q samplerunning; then
echo "✓ Container is running"
CONTAINER_ID=$(podman ps --filter name=samplerunning --format '{{.ID}}')
echo "Container ID: ${CONTAINER_ID}"
echo "To test: curl -f http://localhost:8081"
echo "Service will persist after Jenkins job completion"
else
echo "✗ Container not running despite service being active"
systemctl --user status "${SERVICE_NAME}" --no-pager
exit 1
fi
else
echo "✗ Quadlet service failed to start"
systemctl --user status "${SERVICE_NAME}" --no-pager
exit 1
fi
exit 0
```
Remember to commit and push this new Bash script to your Git repository before launching a new build job on the Jenkins service.
### Step 6: Launch the Build Job again
Click the **Build Now** tab on the left panel of the Jenkins service webpage.
Below is a copy of the **Build job** console output based on the new script, which uses a dedicated systemd Quadlet service to run the container autonomously from Jenkins.
```=
Démarré par l'utilisateur Jenkins Admin
Exécution en tant que SYSTEM
Construction à distance sur
debian-agent (linux) dans le répertoire de travail /home/jenkins-agent/workspace/Build_Sample_App_Job
The recommended git tool is: NONE
using credential 568eb0df-27f0-42fd-a003-d5a176a4f60d
Cloning the remote Git repository
Cloning repository git@gitlab.inetdoc.net:devnet/lab12.git
> git init /home/jenkins-agent/workspace/Build_Sample_App_Job # timeout=10
Fetching upstream changes from git@gitlab.inetdoc.net:devnet/lab12.git
> git --version # timeout=10
> git --version # 'git version 2.51.0'
using GIT_SSH to set credentials Jenkins service Git credentials
Verifying host key using known hosts file
> git fetch --tags --force --progress -- git@gitlab.inetdoc.net:devnet/lab12.git +refs/heads/*:refs/remotes/origin/* # timeout=10
> git config remote.origin.url git@gitlab.inetdoc.net:devnet/lab12.git # timeout=10
> git config --add remote.origin.fetch +refs/heads/*:refs/remotes/origin/* # timeout=10
Avoid second fetch
> git rev-parse refs/remotes/origin/main^{commit} # timeout=10
Checking out Revision 2cf242c396c44cd899ac99d41965025fe6cad68c (refs/remotes/origin/main)
> git config core.sparsecheckout # timeout=10
> git checkout -f 2cf242c396c44cd899ac99d41965025fe6cad68c # timeout=10
Commit message: "Amélioration de la clarté et de la structure des scripts, mise à jour des messages et suppression de fichiers obsolètes"
First time build. Skipping changelog.
[Build_Sample_App_Job] $ /bin/sh -xe /tmp/jenkins9511993452201026134.sh
+ bash ./sample-app.sh
Building container image...
STEP 1/8: FROM docker.io/library/python
Trying to pull docker.io/library/python:latest...
Getting image source signatures
Copying blob sha256:4f69a3eb488f29441ffb9cc440e3fe2a3173f38608dd0062d949bcd8ede35514
Copying blob sha256:ef235bf1a09a237b896b69935c8c8d917c9c6a78b538724911414afc0a96763c
Copying blob sha256:954d6059ca7bdbb9ceb566ca2239e01ef312165659d656753d7dbace7771a591
Copying blob sha256:d8871274053b4c4ae1362d039b370d40afa86fa1463d66808866ab5e86ef711f
Copying blob sha256:b5e2021c4c8bd1a46b34d9608a9381afdc333600ee1ef3c94306ecf7373e1956
Copying blob sha256:128c712640095cd6361adb8c415d18f40180beef843ae18943d3a366993d7749
Copying blob sha256:c59471c320a2f6b05d95c22f912d3123bf8b8b4c84b3358116d57361ce77233d
Copying config sha256:9a7c5808942da8a33df72836a2ed9f1080cfe589aa94d3cab1de7437043424b3
Writing manifest to image destination
STEP 2/8: ENV PIP_ROOT_USER_ACTION=ignore
--> d184924c3712
STEP 3/8: RUN pip3 install --upgrade flask
Collecting flask
Downloading flask-3.1.2-py3-none-any.whl.metadata (3.2 kB)
Collecting blinker>=1.9.0 (from flask)
Downloading blinker-1.9.0-py3-none-any.whl.metadata (1.6 kB)
Collecting click>=8.1.3 (from flask)
Downloading click-8.3.1-py3-none-any.whl.metadata (2.6 kB)
Collecting itsdangerous>=2.2.0 (from flask)
Downloading itsdangerous-2.2.0-py3-none-any.whl.metadata (1.9 kB)
Collecting jinja2>=3.1.2 (from flask)
Downloading jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB)
Collecting markupsafe>=2.1.1 (from flask)
Downloading markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (2.7 kB)
Collecting werkzeug>=3.1.0 (from flask)
Downloading werkzeug-3.1.5-py3-none-any.whl.metadata (4.0 kB)
Downloading flask-3.1.2-py3-none-any.whl (103 kB)
Downloading blinker-1.9.0-py3-none-any.whl (8.5 kB)
Downloading click-8.3.1-py3-none-any.whl (108 kB)
Downloading itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Downloading jinja2-3.1.6-py3-none-any.whl (134 kB)
Downloading markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (23 kB)
Downloading werkzeug-3.1.5-py3-none-any.whl (225 kB)
Installing collected packages: markupsafe, itsdangerous, click, blinker, werkzeug, jinja2, flask
Successfully installed blinker-1.9.0 click-8.3.1 flask-3.1.2 itsdangerous-2.2.0 jinja2-3.1.6 markupsafe-3.0.3 werkzeug-3.1.5
[notice] A new release of pip is available: 25.3 -> 26.0.1
[notice] To update, run: pip install --upgrade pip
--> 0c2bf2fbc502
STEP 4/8: COPY ./static /home/myapp/static/
--> 8a7071d2d00b
STEP 5/8: COPY ./templates /home/myapp/templates/
--> 4663666069ba
STEP 6/8: COPY sample-app.py /home/myapp/
--> 35665505404c
STEP 7/8: EXPOSE 8081
--> a727475f86a7
STEP 8/8: CMD python3 /home/myapp/sample-app.py
COMMIT sampleapp
--> 852418899b3e
Successfully tagged localhost/sampleapp:latest
852418899b3e6c1bab1bee40a6bd22b6f8855ab6687aac36872290ab8ca205ca
Cleaning up existing container...
Creating Quadlet configuration...
Reloading systemd and starting service...
Starting Quadlet service...
✓ Quadlet service started successfully
✓ Container is running
Container ID: 1382c474cc36
To test: curl -f
http://localhost:8081
Service will persist after Jenkins job completion
Finished: SUCCESS
```
This time, the **Build job** is successful and the container service is active. Opening the worker virtual machine console with the `jenkins-agent` user identity allows you to verify that the container is running and the web service is available.
```bash
jenkins-agent@worker:~$ podman ps -a
```
```bash=
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4a040da9605f localhost/sampleapp:latest /bin/sh -c python... 7 minutes ago Up 7 minutes 0.0.0.0:8081->8081/tcp samplerunning
```
```bash
curl localhost:8081
```
```html=
<html>
<head>
<title>Sample app</title>
<link rel="stylesheet" href="/static/style.css" />
</head>
<body>
<h1>You are calling me from 2001:678:3fc:VVV:baad:caff:fefe:YYY</h1>
</body>
</html>
```
## Part 6: Use Jenkins to check the application status
In this part, you will create a second job to verify that the application is running properly and that the associated service remains active.
### Step 1: Create a new job to test your sample application.
1. Return to the Jenkins browser tab and click the **Jenkins** link in the top left corner to go back to the main dashboard.
2. Click the **New Item** link to create a new job. In the **Enter an item name** field, enter **Test_Sample_App_Job**.
3. Select **Freestyle Project** as the job type.
4. Scroll to the bottom and click **OK**.
### Step 2: Configure the Test_Sample_App_Job
1. In the **General** tab, add a description for your job. For example, “My sample application test job”.
2. Select **Restrict where this project can be run** and enter **linux** as the **Label Expression**.
3. Go to the **Source Control** tab and select the **Git** radio button. In the **Repository URL** field, enter your GitLab repository link for the sample application:
> git@gitlab.inetdoc.net:CohortXXXXX/SampleUser/ProjectName.git
4. For **Credentials**, select `jenkins-agent (Jenkins agent Git credentials)`, which you created in the previous part.
5. Go to the Git **Branch Specifier** field and make sure the Git branch name is **main** and not master.
6. Go to the Triggers tab and select **Build after other projects are built**, and add **Build_Sample_App_Job** in the **Projects to watch** field.
Click on **Trigger only if build is stable**.
7. Go to the **Build Steps** tab and click **Add build step**. Then, choose **Execute shell**. In the **Command** field, enter the command to run the `test-app-status.sh` script.
```bash
bash ./test-app-status.sh
```
That's it for the Test job configuration! You can click on the Save button.
### Step 3: Create the `test-app-status.sh` Bash script on the worker virtual machine
In this step, you will create a Bash script called `test-app-status.sh` on the worker virtual machine. This script checks whether the sample application container is running and confirms that the corresponding systemd service is active.
Key steps performed by the script.
- Container Status Check:
- The script uses podman ps to check if a container named samplerunning is currently running.
- It reports whether the container is active.
- Application Accessibility Test:
- The script attempts to access the application at `http://localhost:8081` using cURL.
- The script logs whether the application responds successfully.
- Status Summary and Diagnostics:
The script summarizes the results, indicating:
- If both the container and service are operational, the script exits successfully.
- If the container is running but the service is not accessible, the script displays the last ten lines of the container's logs to help with troubleshooting.
- If the container is not running, the script checks to see if it exists but is stopped. If so, it shows the container's status and recent logs.
- Any unexpected state is flagged as an error.
Here is the `test-app-status.sh` script that you will store and run on the worker VM:
```bash=
#!/usr/bin/env bash
# Simple test script for Jenkins pipeline
set -euo pipefail
CONTAINER_NAME="samplerunning"
APP_URL="http://localhost:8081"
echo "=== Application Status Check ==="
if ! command -v podman >/dev/null 2>&1; then
echo "Error: podman not found in PATH"
exit 127
fi
if ! command -v curl >/dev/null 2>&1; then
echo "Error: curl not found in PATH"
exit 127
fi
# 1. Check if container exists and is running
echo "Checking container..."
RUNNING_CONTAINERS=$(podman ps --filter "name=^${CONTAINER_NAME}$" --filter status=running --format "{{.Names}}")
if grep -qx "${CONTAINER_NAME}" <<<"${RUNNING_CONTAINERS}"; then
echo "✓ Container ${CONTAINER_NAME} is running"
CONTAINER_RUNNING=true
else
echo "✗ Container ${CONTAINER_NAME} not found or stopped"
CONTAINER_RUNNING=false
fi
# 2. Check HTTP connectivity
echo "Testing HTTP connectivity..."
if curl -f -s --max-time 5 "${APP_URL}" >/dev/null 2>&1; then
echo "✓ Application accessible at ${APP_URL}"
SERVICE_ACCESSIBLE=true
else
echo "✗ Application not accessible at ${APP_URL}"
SERVICE_ACCESSIBLE=false
fi
# 3. Status summary
echo ""
echo "=== Status Summary ==="
echo "Container active: ${CONTAINER_RUNNING}"
echo "Service accessible: ${SERVICE_ACCESSIBLE}"
if [[ ${CONTAINER_RUNNING} == "true" && ${SERVICE_ACCESSIBLE} == "true" ]]; then
echo "✓ Application fully operational"
exit 0
elif [[ ${CONTAINER_RUNNING} == "true" && ${SERVICE_ACCESSIBLE} == "false" ]]; then
echo "⚠ Container active but service not accessible"
echo "Container logs:"
podman logs --tail=10 "${CONTAINER_NAME}"
exit 1
elif [[ ${CONTAINER_RUNNING} == "false" ]]; then
echo "✗ Container not active"
# Check if it exists but is stopped
ALL_CONTAINERS=$(podman ps -a --filter "name=^${CONTAINER_NAME}$" --format "{{.Names}}")
if grep -qx "${CONTAINER_NAME}" <<<"${ALL_CONTAINERS}"; then
echo "Container exists but is stopped"
echo "Status:"
podman ps -a --filter name="${CONTAINER_NAME}"
echo "Recent logs:"
podman logs --tail=10 "${CONTAINER_NAME}"
else
echo "Container does not exist"
fi
exit 1
else
echo "✗ Unexpected state"
exit 1
fi
```
### Step 4: Launch the test job
From the Jenkins dashboard, select **Test_Sample_App_Job** and click **Build Now** to run the job.
When the build completes, open the **Console Output** and review the log messages to confirm that the container is running and that the application endpoint is reachable.
Below is an example of the Build job console output.
```=
Démarré par l'utilisateur Jenkins Admin
Exécution en tant que SYSTEM
Construction à distance sur
debian-agent (linux) dans le répertoire de travail /home/jenkins-agent/workspace/Test_Sample_App_Job
The recommended git tool is: NONE
using credential 568eb0df-27f0-42fd-a003-d5a176a4f60d
> git rev-parse --resolve-git-dir /home/jenkins-agent/workspace/Test_Sample_App_Job/.git # timeout=10
Fetching changes from the remote Git repository
> git config remote.origin.url git@gitlab.inetdoc.net:devnet/lab12.git # timeout=10
Fetching upstream changes from git@gitlab.inetdoc.net:devnet/lab12.git
> git --version # timeout=10
> git --version # 'git version 2.51.0'
using GIT_SSH to set credentials Jenkins service Git credentials
Verifying host key using known hosts file
> git fetch --tags --force --progress -- git@gitlab.inetdoc.net:devnet/lab12.git +refs/heads/*:refs/remotes/origin/* # timeout=10
> git rev-parse refs/remotes/origin/main^{commit} # timeout=10
Checking out Revision fadb044751b10b157aa839bffecde1f8edd304cb (refs/remotes/origin/main)
> git config core.sparsecheckout # timeout=10
> git checkout -f fadb044751b10b157aa839bffecde1f8edd304cb # timeout=10
Commit message: "Ajout de vérifications pour la présence de podman et curl dans le script test-app-status.sh, amélioration de la robustesse des vérifications de conteneur."
> git rev-list --no-walk 2cf242c396c44cd899ac99d41965025fe6cad68c # timeout=10
[Test_Sample_App_Job] $ /bin/sh -xe /tmp/jenkins14298559711339823295.sh
+ bash ./test-app-status.sh
=== Application Status Check ===
Checking container...
✓ Container samplerunning is running
Testing HTTP connectivity...
✓ Application accessible at
http://localhost:8081
=== Status Summary ===
Container active: true
Service accessible: true
✓ Application fully operational
Finished: SUCCESS
```
No wrong success indicators this time! The container is running, and the web service is accessible.
Here is the `samplerunning`container webpage accessed from the DevNet VM:
```bash
curl http://[2001:678:3fc:VVVV:baad:caff:fefe:YYYY]:8081
```
```html=
<html>
<head>
<title>Sample app</title>
<link rel="stylesheet" href="/static/style.css" />
</head>
<body>
<h1>You are calling me from 2001:678:3fc:VVVV:baad:caff:fefe:YYYY</h1>
</body>
</html>
```

Here are the Podman logs for the container:
```bash
jenkins-agent@worker:~$ podman logs samplerunning
```
```bash=
* Serving Flask app 'sample-app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (::)
* Running on http://[::1]:8081
* Running on http://[2001:678:3fc:VVVV:baad:caff:fefe:YYYY]:8081
Press CTRL+C to quit
2001:678:3fc:VVVV:baad:caff:fefe:YYYY - - [08/Feb/2026 15:15:22] "GET / HTTP/1.1" 200 -
fe80::baad:caff:fefe:0 - - [09/Feb/2026 07:16:22] "GET / HTTP/1.1" 200 -
fe80::baad:caff:fefe:0 - - [09/Feb/2026 07:16:22] "GET /static/style.css HTTP/1.1" 200 -
fe80::baad:caff:fefe:0 - - [09/Feb/2026 07:16:23] "GET /favicon.ico HTTP/1.1" 404 -
2001:678:3fc:VVVV:baad:caff:fefe:YYYY - - [09/Feb/2026 07:28:25] "GET / HTTP/1.1" 200 -
2001:678:3fc:VVVV:baad:caff:fefe:YYYY - - [09/Feb/2026 07:32:27] "GET / HTTP/1.1" 200 -
```
## Part 7: Create a pipeline in Jenkins
In this part, you will create a Jenkins pipeline that chains the build and test jobs into a single, automated CI/CD workflow. The pipeline will first build and deploy the application container, then verify that the application is running correctly.
While you can currently run your two jobs by clicking the **Build Now** button for the **Build_Sample_App_Job**, software development projects are usually much more complex. These projects can greatly benefit from automated builds for continuous integration of code changes and continuous creation of development builds ready for deployment. This is the essence of CI/CD. A pipeline can be automated to run based on various triggers, such as periodically, based on a GitLab poll for changes, or from a remotely run script. In this section, however, you will script a Jenkins pipeline to run your two apps when you click **Build Now**.
### Step 1: Create a Pipeline job
Click the Jenkins link in the top left corner, then click New Item.
1. From the Jenkins dashboard, click **New Item**.
2. In the **Enter an item name** field, type **Sample_App_Pipeline**.
3. Select **Pipeline** as the job type.
4. Scroll down and click **OK**.
### Step 2: Configure the Sample_App_Pipeline
In the job configuration window, you will link the pipeline to your Git repository and tell Jenkins where to find the pipeline definition.
1. In the **General** tab, add a description such as “Pipeline to build and test the sample application”.
2. Go to the **Pipeline** section at the bottom of the page.
3. For **Definition**, select **Pipeline script from SCM**.
4. For **SCM**, choose **Git**.
5. In the **Repository URL** field, enter your GitLab repository URL for the sample application.
6. For **Credentials**, select the Git credentials that allow Jenkins to access your repository.
7. In the **Script Path** field, enter `Jenkinsfile`.

That's it for the Pipeline job configuration! You can now click on the **Save** button.
### Step 3: Create the `Jenkinsfile` in your Git repository
Next, you will create a `Jenkinsfile` in your Git repository that defines the stages of the pipeline.
The **Jenkinsfile** defines a declarative pipeline that automates the build, testing, and deployment of a containerized application using Podman and systemd user services on a Linux agent. The pipeline is triggered by polling the source code management (SCM) system every five minutes for changes.
The pipeline code key features are:
Agent Specification
: Runs exclusively on nodes labeled linux.
Triggers
: Uses pollSCM('H/5 * * * \*') to check for code changes every 5 minutes.
Environment Variables
: CONTAINER_NAME and SERVICE_NAME are both set to samplerunning for consistent reference throughout the pipeline
Here is the table showing the pipeline stages:
| Stage | Purpose | Key Actions |
| ----- | ------- | ----------- |
| Preparation | Cleans up any existing containers and services before starting a new build | Stops/removes Podman containers, stops systemd user service, removes old Quadlet files |
| Checkout | Retrieves the latest source code from SCM | Checks out code, ensures shell scripts are executable |
| Build | Builds the application container | Run `sample-app.sh` script to build the container |
| Test | Runs application tests | Run `test-app-status.sh` to validate the application |
| Verify Persistence | Confirms the application and systemd service are running and accessible | Checks systemd service status, tests HTTP endpoint for accessibility and persistence |
The Jenkinsfile pipeline completes with **post actions**:
- Always: Logs pipeline completion.
- On success: Confirms successful deployment and provides the application URL (`http://worker:8081`).
- On failure: It gathers diagnostic information, including container status, logs, and systemd service status, to aid in troubleshooting.
Here is the `Jenkinsfile` code:
```groovy=
pipeline {
agent {
label 'linux' // Restrict where this pipeline can be run
}
triggers {
pollSCM('H/5 * * * *') // Poll SCM every 5 minutes for changes
}
environment {
CONTAINER_NAME = 'samplerunning'
SERVICE_NAME = 'samplerunning'
}
stages {
stage('Preparation') {
steps {
echo 'Preparing environment...'
script {
// Clean up existing container if it exists
sh '''
echo "Current user: $(whoami)"
echo "Working directory: $(pwd)"
# Stop container if running
if [ -n "$(podman ps --filter "name=^${CONTAINER_NAME}$" --filter status=running --quiet)" ]; then
echo "Stopping existing container..."
podman stop "${CONTAINER_NAME}" || true
# Wait a moment for container to stop completely
sleep 2
fi
# Remove container if it exists
if [ -n "$(podman ps -a --filter "name=^${CONTAINER_NAME}$" --quiet)" ]; then
echo "Removing existing container..."
podman rm "${CONTAINER_NAME}" || true
fi
# Stop systemd service if active
if systemctl --user is-active "${SERVICE_NAME}" --quiet 2>/dev/null; then
echo "Stopping systemd service..."
systemctl --user stop "${SERVICE_NAME}" || true
fi
# Remove Quadlet file if it exists
if [ -f ~/.config/containers/systemd/samplerunning.container ]; then
echo "Removing existing Quadlet file..."
rm ~/.config/containers/systemd/samplerunning.container
systemctl --user daemon-reload
fi
'''
}
}
}
stage('Checkout') {
steps {
echo 'Checking out source code...'
checkout scm
// Make scripts executable after checkout
sh '''
echo "Making scripts executable..."
chmod +x *.sh
echo "Scripts permissions:"
ls -la *.sh
echo "Files in directory:"
ls -la
'''
}
}
stage('Build') {
steps {
echo 'Building application container...'
build job: 'Build_Sample_App_Job'
}
}
stage('Test') {
steps {
echo 'Testing application...'
build job: 'Test_Sample_App_Job'
}
}
stage('Verify Persistence') {
steps {
echo 'Verifying service persistence...'
sh '''
# Wait a moment for service to stabilize
sleep 5
# Check if systemd service is active
if systemctl --user is-active "${SERVICE_NAME}" --quiet; then
echo "✓ Systemd service is active"
else
echo "✗ Systemd service is not active"
systemctl --user status "${SERVICE_NAME}" --no-pager || true
exit 1
fi
# Final HTTP test
if curl -f -s --max-time 10 http://localhost:8081 > /dev/null; then
echo "✓ Application is accessible and will persist after job completion"
else
echo "✗ Application is not accessible"
exit 1
fi
'''
}
}
}
post {
always {
echo 'Pipeline completed'
}
success {
echo '✓ Application deployed successfully and will persist after job completion'
echo 'Application is accessible at: http://worker:8081'
}
failure {
echo '✗ Pipeline failed'
script {
// Gather diagnostic information on failure
sh '''
echo "=== Diagnostic Information ==="
echo "Container status:"
podman ps -a --filter name="${CONTAINER_NAME}" || true
echo "Container logs:"
podman logs "${CONTAINER_NAME}" --tail=20 || true
echo "Systemd service status:"
systemctl --user status "${SERVICE_NAME}" --no-pager || true
'''
}
}
}
}
```
Commit and push the `Jenkinsfile` to your Git repository so that Jenkins can load it the next time the pipeline runs.
### Step 4: Run the Sample App Pipeline
From the Jenkins dashboard, click **Sample_App_Pipeline** and then **Build Now** to start the pipeline.
Open the **Stage View** or **Console Output** to follow the execution of each stage and verify that both the build and test steps complete successfully.
To see an overview of the results, select a pipeline execution number, then click the "Pipeline Overview" tab on the left.

Clicking on a stage in the horizontal graphic displays the detailed results of that stage.
Here is a copy of the **Verify Persistence** stage output:

## Conclusion
This lab guided you through setting up a complete CI/CD pipeline using Jenkins and Podman. You learned how to build, deploy, and test a containerized web application to ensure reliable service, persistence, and repeatable deployments. You created separate build and test jobs, combining them into a CI/CD pipeline that validates each code change and keeps the application ready for production. These concepts can be applied to larger applications and more complex deployment environments, such as multi-service architectures or production-grade clusters.