An introduction to Jenkins - the CI/CD tool
Tags: oracle, cloud, jenkins
Published: 2023-06-22 02:36:55

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](https://www.jenkins.io/doc/book/installing/macos).

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

![Jenkins homepage screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wkfa0j3vrjksjn93c1h0.png)

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ddw7kbvmcx3mh7qflei8.png)

Put something like:
```sh
echo "hello123"
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h2gh4rvbxaohdf6p46js.png)

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

![hello123 being echoed in console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0bwcq3dr3os51fi2ldm8.png)

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pi122e4sfuzay8illwf0.png)

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](https://dev.to/farisdurrani/pushing-docker-image-to-oracle-container-registry-5kl).

![Adding Docker login username password](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6wlf8nkews9c87jzo4el.png)

## A full Docker push example with Jenkinsfile

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

```groovy
// 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"
    }
}
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sak1by6jbwv6nn88h15w.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d971nne5b54a2icpn3wv.png)


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


![Many Jenkins build jobs example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z7e3bxmgpr1a2d31fqeo.png)

### Important: Docker permissions
You may encounter this issue when using Docker in Jenkins:
```sh
docker: Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock
```
To solve this I refer to this [StackOverflow](https://stackoverflow.com/a/51362528/11031425) answer which suggests running in your native terminal every time your computer starts up:
```sh
sudo chmod 666 /var/run/docker.sock
```

### 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,
```txt
H/30 * * * *
```
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](http://creativecommons.org/licenses/by/4.0/).*