253 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 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 the application is always ready for deployment whenever code changes occur ![Lab12 topology](https://md.inetdoc.net/uploads/852929be-2644-441c-8756-a8afce4f2b12.png) ### 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: Login to GitLab and create a new repository - Login 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 should just be a 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, it's time to fix them. 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 you have all your lab files stored in subdirectories of `$HOME/labs`, we 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. ``` We are now ready to open this new folder in Visual Studio Code. ### Step 4: Stage, commit, and push the sample app files to the GitLab repository Here we start by copying the previous lab `sample-app` directory. Then we add this directory to the git repository after removing the `tempdir` directory that was used for temporary files. ```bash cp -ar ../lab11/sample-app/* . rm -rf tempdir ``` ```bash= tree -L 2 . ├── README.md ├── sample-app.py ├── sample-app.sh ├── static │ └── style.css ├── templates └── index.html ``` We 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 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 Jenkins service on the DevNet VM In this part, we 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. More than just installing a package, we need to configure two user identities with passwordless SSH authentication from the 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 version "21.0.7" 2025-04-15 OpenJDK Runtime Environment (build 21.0.7+6-Ubuntu-0ubuntu125.04) OpenJDK 64-Bit Server VM (build 21.0.7+6-Ubuntu-0ubuntu125.04, mixed mode, sharing) ``` Once the Java installation is verified, we move on to package management and adding a new repository key, 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-2023.key |\ sudo tee /usr/share/keyrings/jenkins-keyring.asc > /dev/null ``` 2. Add the Jenkins packages 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:109:112:Jenkins,,,:/var/lib/jenkins:/bin/bash ``` ### Step 2: Prepare `jenkins` user account for SSH communication with the worker node We start by assigning a passwword 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= total 44 -rw-r--r-- 1 jenkins jenkins 1658 avril 1 16:57 config.xml -rw-r--r-- 1 jenkins jenkins 156 avril 1 16:57 hudson.model.UpdateCenter.xml -rw-r--r-- 1 jenkins jenkins 171 avril 1 16:57 jenkins.telemetry.Correlator.xml drwxr-xr-x 2 jenkins jenkins 4096 avril 1 16:57 jobs -rw-r--r-- 1 jenkins jenkins 1037 avril 1 16:57 nodeMonitors.xml drwxr-xr-x 2 jenkins jenkins 4096 avril 1 16:57 plugins -rw-r--r-- 1 jenkins jenkins 64 avril 1 16:57 secret.key -rw-r--r-- 1 jenkins jenkins 0 avril 1 16:57 secret.key.not-so-secret drwx------ 2 jenkins jenkins 4096 avril 1 16:57 secrets drwxr-xr-x 2 jenkins jenkins 4096 avril 1 16:57 updates drwxr-xr-x 2 jenkins jenkins 4096 avril 1 16:57 userContent drwxr-xr-x 3 jenkins jenkins 4096 avril 1 16:57 users ``` :::info Notice the presence of a directory called `secrets` which contains a file called **initialAdminPassword** which we will use for the first web service authentication. ::: ```bash ls -l secrets ``` ```bash= total 12 -rw-r----- 1 jenkins jenkins 33 avril 1 16:57 initialAdminPassword -rw-r--r-- 1 jenkins jenkins 32 avril 1 16:57 jenkins.model.Jenkins.crumbSalt -rw-r--r-- 1 jenkins jenkins 256 avril 1 16:57 master.key ``` Using this `jenkins` user account, we are ready to run a one-liner 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 node and create the `jenkins-agent` user account Starting a new Jenkins service worker virtual machine to launch Podman containers requires a 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 hasn't already been created, we need to start the worker VM first to add the new `jenkins-agent` user account. So we need to create two YAML declaration files, one for the network connection and one for the VM itself. 1. Here is a sample network connection declaration file, which can be named `lab12-switch.yaml`: ```yaml= ovs: switches: - name: dsw-host ports: - name: tapYYY # <- YOUR OWN TAP INTERFACCE 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 INTERFACCE NUMBER ``` :::warning Don't forget to replace the placeholder 'YYY' with your own virtual machine tap interface number. ::: Once these two files are created on the hypervisor, we can call the two scripts to start the worker VM from the hypervisor terminal. ```bash $HOME/masters/scripts/switch-conf.py lab12-switch.yaml $HOME/masters/scripts/lab-startup.py lab12-worker.yaml ``` Let's copy the SSH public key of our DevNet user to the worker virtual machine. ```bash ssh-copy-id etu@fe80::baad:caff:fefe:YYY%enp0s1 ``` ```bash= /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 Warning: Permanently added 'fe80::baad:caff:fefe:YYY%enp0s1' (ED25519) to the list of known hosts. (etu@fe80::baad:caff:fefe:2%enp0s1) Password: Number of key(s) added: 1 Now try logging into the machine, with: "ssh 'etu@fe80::baad:caff:fefe:YYY%enp0s1'" and check to make sure that only the key(s) you wanted were added. ``` Then we can add a new entry to the DevNet VM ssh client configuration file. ```bash= cat << EOF >>$HOME/.ssh/config Host worker HostName fe80::baad:caff:fefe:YYY%%enp0s1 User etu Port 22 EOF ``` Finally, we will test the passwordless SSH connection to this worker node. ```bash ssh -tt -o StrictHostKeyChecking=accept-new worker \ "sudo hostnamectl hostname worker && \ echo '# etu is an admin user account etu ALL=(ALL:ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/etu && \ sudo reboot" ``` ```bash= [sudo] Mot de passe de etu : Connection to fe80::baad:caff:fefe:YYY%enp0s1 closed. ``` Notice that we have added two important configurations through this SSH connection test: * We changed the virtual machine hostname to **worker**. * We added the ability for the default user account to run the `sudo` command without prompting for a password. The connection test is completed by rebooting the worker virtual machine. ```bash ssh worker ``` ```bash= Linux worker 6.12.27-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.27-1 (2025-05-06) x86_64 The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright. Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. Last login: Thu May 22 15:33:27 2025 from fe80::baad:caff:fefe:XXX%enp0s1 etu@worker:~$ ``` Within this SSH open connection, install the **default-jdk-headless** package that will be required when you add this virtual machine as a Jenkins service node. ```bash sudo apt install default-jdk-headless exit ``` ### Step 2: Create the `jenkins-agent` user account and install Podman remotely From the DevNet VM, we run a locally developed bash script over an SSH connection. Here is the code of the 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 # This script is to be run this way: # ssh worker '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 if jenkins agent user is linger enabled 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, we can run the following command to run it remotely on the worker node virtual machine. ```bash ssh worker 'sudo bash -s' < setup-jenkins-agent.sh ``` We will add another command to retrieve the `jenkins-agent` password for testing purposes. ```bash ssh worker "sudo cat /root/.jenkins-agent.passwd" >$HOME/.jenkins-agent.passwd cat $HOME/.jenkins-agent.passwd |\ sudo tee /var/lib/jenkins/secrets/worker.jenkins-agent.passwd sudo chown jenkins:jenkins /var/lib/jenkins/secrets/worker.jenkins-agent.passwd sudo chmod 600 /var/lib/jenkins/secrets/worker.jenkins-agent.passwd ``` ### Step 3: Configure passwordless SSH connections from the Jenkins service to the agent on the worker VM This secret transmission allows us to open an SSH connection to the new `jenkins-agent` user account on the worker node. Before initiating this new SSH connection, we need to obtain the Jenkins service identity, which means opening a session with the `jenkins` user account on the DevNet VM itself. ```bash su - jenkins ``` Once the `jenkins` user session is open, we 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 worker HostName fe80::baad:caff:fefe:YYY%%enp0s1 User jenkins-agent Port 22 EOF ``` We 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. ```bash sshpass -f $HOME/secrets/worker.jenkins-agent.passwd \ ssh -o StrictHostKeyChecking=accept-new worker ``` ```bash= Linux worker 6.12.27-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.27-1 (2025-05-06) x86_64 The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright. Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. Last login: Sat May 24 10:16:38 2025 from fe80::baad:caff:fefe:0%enp0s1 jenkins-agent@worker:~$ loginctl list-users UID USER LINGER STATE 1000 etu yes lingering 1001 jenkins-agent yes active 2 users listed. ``` Finally, to avoid using the sshpass command, we need to copy the Jenkins service SSH public key to the worker node. ```bash sshpass -f $HOME/secrets/worker.jenkins-agent.passwd \ ssh-copy-id worker ``` ```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 'worker'" and check to make sure that only the key(s) you wanted were added. ``` ## Part 4: Configure the Jenkins service In this part, you will complete the initial configuration of the Jenkins server. ### 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. We know that the Jenkins web service is listening on port 8080 on the DevNet VM. Therefore, we need to forward the local port 8080 to the DevNet VM via the SSH connection. We can do this 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 your own virtual machine address. ::: Once the SSH connection is open, navigate to `http://localhost:8080/` and log in with the Admin 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 in 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: **fe80::baad:caff:fefe:YYY%%enp0s1** (Same 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)) - Credentials: **Select the SSH key created earlier** After adding the node agent is complete, the node status information should look like the screenshot below. ![Node status](https://md.inetdoc.net/uploads/7f911f7a-69b8-4053-8eab-c6ede17a411f.png) ## 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 the build script. ### Step 1: Give the `jenkins` user account access to your lab's Git repository Before we dive into configuring the Jenkins service 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 of the `jenkins` user to your GitLab account. 1. Make a copy of the SSH public key of the `jenkins` user ```bash jenkins-agent@worker:~$ cat .ssh/id_ed25519.pub 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, @etudiantteststri! ``` ### 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 jenkins-agent@worker:~$ cat .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/6bd6b019-f223-4c33-ac28-979608e574d7.png) 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 ``` Let's check the container's state on the worker virtual machine. ```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 Upon investigating exit code 143, we discovered that the container started properly but was stopped at the end of the Jenkins job. This is a common occurrence when building containers remotely. Therefore, we 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 to the previous version. :::info A **systemd quadlet service** manages Podman containers (and related resources) as native systemd services using a simplified, declarative configuration file. Quadlets bridge Podman and systemd, making it easier to run containers as systemd-managed background services with all the benefits of systemd, such as automatic startup, dependency management, logging, and health monitoring. ::: ```bash= #!/bin/bash set -e # Ensure podman is properly configured for rootless operation XDG_RUNTIME_DIR="/run/user/$(id -u)" export XDG_RUNTIME_DIR mkdir -p tempdir 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 code 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 code, which uses a systemd quadlet dedicated service to run the container service autonomously from Jenkins. ```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 c6aa6d1fa0319b8d3f4e1a027a62a3d430e9e209 # timeout=10 [Build_Sample_App_Job] $ /bin/sh -xe /var/tmp/jenkins343581539439698001.sh + bash ./sample-app.sh Building container image... 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 Cleaning up existing container... samplerunning Creating Quadlet configuration... Reloading systemd and starting service... Starting Quadlet service... ✓ Quadlet service started successfully ✓ Container is running Container ID: 4a040da9605f 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 us to check that the container status is up 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 status of the application In this part, you will create a second job to verify that the application is functioning properly , and that the associated service is active. ### Step 1: Create a new job to test your sample application. 1. Return to the Jenkins web browser tab and click the **Jenkins** link in the top left corner to return 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 the link to your GitLab repository link for the sample application: > git@gitlab.inetdoc.net:CohortXXXXX/SampleUser/ProjectName.git 4. For Credentials, select the "jenkins-agent (Jenkins agent Git credentials)" 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 The Bash script below automates basic health checks for containerized applications. Its primary function is to verify that a specific container is running and that the application within is accessible via HTTP. 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. ```bash= #!/bin/bash # Simple test script for Jenkins pipeline set -e CONTAINER_NAME="samplerunning" APP_URL="http://localhost:8081" echo "=== Application Status Check ===" # 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 echo "${RUNNING_CONTAINERS}" | grep -q "^${CONTAINER_NAME}$"; 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 echo "${ALL_CONTAINERS}" | grep -q "^${CONTAINER_NAME}$"; 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 Build Job Click **Build Now** on the left to start the job. Below is a copy of the Build job console output. ```bash= Started by user jenkins Admin Running as SYSTEM Building remotely on Debian Worker (linux) in workspace /home/jenkins-agent/workspace/Test_Sample_App_Job Selected Git installation does not exist. Using Default The recommended git tool is: NONE using credential 0b331a3a-874c-40cb-892c-1dff9ede2621 > 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.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 1edad77ddf33851d97f70bc99a78381fb8d997f3 (refs/remotes/origin/main) > git config core.sparsecheckout # timeout=10 > git checkout -f 1edad77ddf33851d97f70bc99a78381fb8d997f3 # timeout=10 Commit message: "Refactor les vérifications de conteneurs et d'images dans les scripts de nettoyage et de test pour utiliser des variables intermédiaires, améliorant ainsi la lisibilité et la performance." > git rev-list --no-walk 11948e304d90675b54127e3d19f834626f5899be # timeout=10 [Test_Sample_App_Job] $ /bin/sh -xe /var/tmp/jenkins1066995911516497753.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:34:baad:caff:fefe:2]: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:XXX</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:34:baad:caff:fefe:2]:8081 Press CTRL+C to quit 2001:678:3fc:VVV:baad:caff:fefe:YYY - - [26/May/2025 16:39:13] "GET / HTTP/1.1" 200 - 2001:678:3fc:VVV:baad:caff:fefe:YYY - - [26/May/2025 16:39:19] "GET / HTTP/1.1" 200 - 2001:678:3fc:VVV:baad:caff:fefe:XXX - - [27/May/2025 13:00:08] "GET / HTTP/1.1" 200 - 2001:678:3fc:VVV:baad:caff:fefe:XXX - - [27/May/2025 13:00:32] "GET / HTTP/1.1" 200 - ``` ## Part 7: Create a pipeline in Jenkins 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. In the "Enter an item name" field, type "Sample App Pipeline". 2. Select **Pipeline** as the job type. 3. Scroll to the bottom and click OK. ### Step 2: Configure the Sample App Pipeline As we did with the previous two jobs, click on the tabs and examine each section of the configuration page. 1. On the General tab, add a description of your job. For example, enter "My Sample Application Pipeline". 3. Go to the **Pipeline** tab and select the **Pipeline script** from **SCM** option, then select **Git** as the SCM tool. In the Repository URL field, enter the link to your GitLab repository link for the sample application: > git@gitlab.inetdoc.net:CohortXXXXX/SampleUser/ProjectName.git 4. For Credentials, select the "jenkins-agent (Jenkins agent Git credentials)" created in the previous part. 5. Go to the Git **Branch Specifier** field and ensure the Git branch name is **main** and not master. 6. 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 The following 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 podman ps --filter name="${CONTAINER_NAME}" --filter status=running --quiet | grep -q .; 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 podman ps -a --filter name="${CONTAINER_NAME}" --quiet | grep -q .; 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...' sh './sample-app.sh' } } stage('Test') { steps { echo 'Testing application...' sh './test-app-status.sh' } } 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 ''' } } } } ``` ### Step 4: Run the Sample App Pipeline To begin, go to the green arrow on the right side of the "Sample App Pipeline" line on the main Jenkins webpage and complete all stages defined in the Jenkinsfile. To see an overview of the results, select a pipeline execution number (#6 for instance), then click the "Pipeline Overview" tab on the left. ![Pipeline overview](https://md.inetdoc.net/uploads/f3ad8be3-88fa-4cd2-b369-544f40d4175b.png) Clicking on a stage in the horizontal graphic displays the detailed results of that stage. We also have access to the Git Polling Log, which provides information about pipeline triggering when the Git repository evolves. Below is an example of a console output showing that there has been no change since pipeline number 6 was run. ```bash= Started on May 27, 2025, 5:41:00 PM Using strategy: Default [poll] Last Built Revision: Revision c9e20f7711b155d2fb18efa92a298f38d7f42d9c (refs/remotes/origin/main) Selected Git installation does not exist. Using Default The recommended git tool is: NONE using credential 0b331a3a-874c-40cb-892c-1dff9ede2621 > git --version # timeout=10 > git --version # 'git version 2.48.1' using GIT_SSH to set credentials Jenkins agent Git credentials Verifying host key using known hosts file > git ls-remote -h -- git@gitlab.inetdoc.net:devnet/lab12.git # timeout=10 Found 1 remote heads on git@gitlab.inetdoc.net:devnet/lab12.git [poll] Latest remote head revision on refs/heads/main is: c9e20f7711b155d2fb18efa92a298f38d7f42d9c - already built by 6 Using strategy: Default [poll] Last Built Revision: Revision c9e20f7711b155d2fb18efa92a298f38d7f42d9c (refs/remotes/origin/main) Selected Git installation does not exist. Using Default The recommended git tool is: NONE using credential 0b331a3a-874c-40cb-892c-1dff9ede2621 > git --version # timeout=10 > git --version # 'git version 2.48.1' using GIT_SSH to set credentials Jenkins agent Git credentials Verifying host key using known hosts file > git ls-remote -h -- git@gitlab.inetdoc.net:devnet/lab12.git # timeout=10 Found 1 remote heads on git@gitlab.inetdoc.net:devnet/lab12.git [poll] Latest remote head revision on refs/heads/main is: c9e20f7711b155d2fb18efa92a298f38d7f42d9c - already built by 6 Done. Took 1.4 sec No changes ``` ## Conclusion This lab guided you through setting up a complete CI/CD pipeline with Jenkins and Podman. You automated the build, deployment, and testing of a containerized web application to ensure reliable service persistence. The result is a robust workflow that streamlines development and validates the readiness of each code change for production. Once again, you can access your "fantastic" website from your personal computer via SSH. First, open a new SSH connection with port forwarding. ```bash ssh -L "8081:[2001:678:3fc:VVV:baad:caff:fefe:YYY]:8081" devnet ``` Then, open a browser tab with the localhost URL. ![Sample App website](https://md.inetdoc.net/uploads/ac77e701-4b8f-4621-8e10-9b3a6dc03f2b.png)