An introduction to Jenkins - the CI/CD tool

Faris Durrani - Jun 22 '23 - - Dev Community

Jenkins is an open-source CI/CD tool widely used in DevOps. This guide goes through installing Jenkins, using secrets in Jenkins, and writing a Jenkinsfile to pull and push a Docker image to an Oracle Cloud Container Registry.

What is CI/CD?

An important part of a good development practice is a good continuous integration/continuous development (CI/CD) practice, which focuses on the deployment of your app's updates for the public to use.

A CI/CD pipeline can be broken into two parts (obviously):

  1. Integration

Deals with integrating the new code to the central codebase. This includes checks (lint/format checks, build checks, basic unit tests, secret exposures) and integration steps to the codebase like pulling in external code and requesting reviews.

2. Deployment

Things like building a Docker image, pushing it, and pushing out deployment update in a safe manner, possibly using A/B testing

A lot of these like running tests are repetitive and can therefore be automated, e.g., on every new code push.

Enter Jenkins.

If you've heard of GitHub Actions, Jenkins is the lower-level, open-source sister of GitHub Actions, allowing far more customizations.

Installing Jenkins

Jenkins is a standalone tool that you can install and deploy on your own, by default on http://localhost:8080.

See installation instructions in the official Jenkins page.

Your Jenkins homepage (localhost:8080) looks somewhat like this:

Jenkins homepage screenshot

Running a basic Shell command in Jenkins

Let's start small and make a build job that prints to the console.

From the homepage, click New Item > Freestyle project (remember to name your project).

Once you created the project and arrive to the Configuration page, scroll down to Build Steps and add an Execute shell step.

Execute shell step in Build Steps

Put something like:

echo "hello123"
Enter fullscreen mode Exit fullscreen mode

and click Save.

At the project page, click Build now and see the Build History appear with your build #1. Click it.

Click build now

Click Console Output and see your echo being printed in the console:

hello123 being echoed in console

Storing Secrets

Storing secrets to later inject into your build is easy in Jenkins. To create a new secret, click on your Profile Picture > Credentials > (global) > Add Credentials.

You can add username/password combinations, text secrets, file secrets, and more.

Let's try adding a .env file as an example. Since on Mac I can't select a .env file from Finder, I'll be renaming it to fenv. Let's give the credential an ID of fd-env-f.

Adding a .env credential

And here, I'll be adding my Oracle Container Registry Docker login username and password i.e. Auth Token. You can read more about using the Container Registry in another article.

Adding Docker login username password

A full Docker push example with Jenkinsfile

Our goal for this last part is to understand this Jenkinsfile:

// Jenkinsfile
pipeline {
    agent any
    stages {
        stage('Clean workspace') {
            steps {
                cleanWs()
            }
        }
        stage('Git pull after each commit') {
            steps {
                git branch: 'dev', credentialsId: 'fd-git-creds', url: 'https://github.com/farisdurrani/nonexistentrepo'
                echo 'pulled successfully'
            }
        }
        stage('Build and Push Docker Frontend Image') {
            steps {
                withCredentials(
                    [file(credentialsId: 'fd-env-f', variable: 'mapEnvF')]
                ) {
                    script {
                        docker.withRegistry(registryUrl, registryCredential) {
                            // create .env file
                            sh "sudo touch ./frontend/.env"
                            sh "sudo chmod 666 ./frontend/.env"
                            sh "cp \"$mapEnvF\" ./frontend/.env"

                            // build image, push to registry, and remove image
                            image = docker.build(
                                "$frontendImage", 
                                "./frontend"
                                )
                            image.push(tagLatest)
                            sh "docker rmi ${image.id}"
                        }
                    }
                }
            }
        }
    }
    environment {
        tenancyNamespace = 'id8wj7jebvoqh'
        registry = 'iad.ocir.io'
        registryCredential = "fd-docker-oci-container-registry"
        registryUrl = "https://$registry/"
        tagPrefix = "$registry/$tenancyNamespace"
        frontendImage = "$tagPrefix/myapp-f:latest"

        // required to save/re-use Yarn cache across multiple runs
        YARN_GLOBAL_FOLDER = "${JENKINS_HOME}/workspace/.yarn/global"
    }
}
Enter fullscreen mode Exit fullscreen mode

This file is the Jenkins build pipeline code of this app, usually stored at the root directory of the app repo and conventionally named Jenkinsfile without extensions.

This file has 3 stages, each with its own steps:

  1. Clean up workspace - delete all previous build files that were created in the previous builds
  2. Git pull - pulling the code from the repo
  3. Build and push Docker image to the Oracle Container Registry

The first two are relatively straightforward. Notice we're using a credentialsId, created in Credentials, to login to the git repo.

The goal of the third step is to build and push the Docker image while injecting a .env file into the image.

And yes, you should be using Docker's built-in --build-arg flag to do this but we're more concerned with the base Jenkins functionality here.

Using the withCredentials function, we are able to pick the file secret using its ID and copy its contents into the Docker image before push. Due to the restrictive nature of Jenkins secrets, a little copy-pasting is necessary to get its raw contents.

Copy-pasting the Jenkinsfile contetns into a new Jenkins Pipeline (not a Freestyle project), we have prepared a proper pipeline.

Adding Jenkinsfile to pipeline

Alternatively, you may also choose to have the Jenkins pipeline refer to a Jenkinsfile stored in a git repo, by choosing Pipeline script from SCM as the Definition of the pipeline:

Pipeline script from SCM

Save and run the job. Here is an example of what many Jenkins build jobs look like:

Many Jenkins build jobs example

Important: Docker permissions

You may encounter this issue when using Docker in Jenkins:

docker: Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock
Enter fullscreen mode Exit fullscreen mode

To solve this I refer to this StackOverflow answer which suggests running in your native terminal every time your computer starts up:

sudo chmod 666 /var/run/docker.sock
Enter fullscreen mode Exit fullscreen mode

Important: " vs ' in Jenkinsfile

There is a strong distinction between double-quotes " and single-quotes ' in a Jenkinsfile. " enables string interpolation, i.e.,"docker rmi ${image.id}" while ' does not, making it return the raw string as it is without substitution.

Building with a schedule

To run the Jenkins build pipeline at a schedule with every code changes, simply add a Poll SCM option under Build Triggers, for example,

H/30 * * * *
Enter fullscreen mode Exit fullscreen mode

to run the build job every 30 minutes. If no changes to the remote code are detected, the pipeline is not executed.


Safe harbor statement
The information provided on this channel/article/story is solely intended for informational purposes and cannot be used as a part of any contractual agreement. The content does not guarantee the delivery of any material, code, or functionality, and should not be the sole basis for making purchasing decisions. The postings on this site are my own and do not necessarily reflect the views or work of Oracle or Mythics, LLC.

This work is licensed under a Creative Commons Attribution 4.0 International License.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player