1633 views
# 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 ![Lab 12 topology diagram](https://md.inetdoc.net/uploads/72e74d85-b8e3-4eaf-a919-846a039689a6.png) 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**. ![Add credentials](https://md.inetdoc.net/uploads/23226fb8-6837-450d-b48e-73843316f7c5.png) 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. ![Node status example](https://md.inetdoc.net/uploads/7f911f7a-69b8-4053-8eab-c6ede17a411f.png) 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. ![Add jenkins user SSH key to GitLab](https://md.inetdoc.net/uploads/d5e98d75-bdeb-4881-916b-791c7cbd8841.png) 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**. ![Assign the worker to this job](https://md.inetdoc.net/uploads/68c04404-cbec-46a6-9d23-470e6e0b9573.png) 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----- ``` ![Add jenkins agent credentials](https://md.inetdoc.net/uploads/05339ad0-5373-47d5-9126-d2c30d7a2e6a.png) 5. There is one last very important Git parameter: **Branch Specifier** Make sure the Git branch name is **main** and not **master**. ![Associate Job to Git repository](https://md.inetdoc.net/uploads/a4d92600-39cb-4ddf-9a08-af5b8d2c03ee.png) 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 ``` ![Build step example](https://md.inetdoc.net/uploads/d7a86783-6565-457a-a011-2fca4a483e62.png) 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> ``` ![Sample app access screenshot](https://md.inetdoc.net/uploads/ba01dfee-a17f-4f7f-a727-58d763d7ef48.png) 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`. ![jenkins pipeline Git configuration](https://md.inetdoc.net/uploads/31ab7df9-c3dd-459c-9421-195f956e5e9d.png) 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. ![Jenkins pipeline stages](https://md.inetdoc.net/uploads/da41e36f-4b65-43ab-88b1-6459b0860a38.png) 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: ![Verify Persistence stage output](https://md.inetdoc.net/uploads/06a1866c-4215-4e79-809e-37455e31dd46.png) ## 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.