Jenkins - Universal CI Pipeline with Ansible & Terraform

"Such a long time I nearly forgot how to use Jenkins.."

Jenkins recap

It has been some time since I adapted CI/CD pipelines from Jenkins to AWS CodePipeline and GitHub Actions workflows. Now, it's time to recap and improve some of my previous Jenkins practices.

  • Universal Jenkins Docker image design

This time, instead of installing Jenkins on a server, I prefer to containerize a universal Jenkins Docker image with the necessary packages installed, so it provides consistency and reproducibility, portability, and easy updates and rollbacks.

  • Docker CLI
  • Terraform
  • Kubectl
  • Trivy
  • AWS CLI
  • Ansible
# Dockerfile

FROM jenkins/jenkins:lts

USER root

# Install necessary packages, Docker CLI, Terraform, Kubectl, Trivy, AWS CLI, and Ansible
RUN apt-get update && \
    apt-get install -y \
        curl \
        wget \
        unzip \
        gnupg2 \
        apt-transport-https \
        lsb-release \
        ca-certificates \
        software-properties-common \
        python3 \
        python3-venv \
        python3-pip && \

    # Install Docker CLI
    curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add - && \
    echo "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list && \
    apt-get update && \
    apt-get install -y docker-ce-cli && \

    # Install Terraform
    wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor > /usr/share/keyrings/hashicorp-archive-keyring.gpg && \
    echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" > /etc/apt/sources.list.d/hashicorp.list && \
    apt-get update && \
    apt-get install -y terraform && \

    # Install Kubectl
    curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && \
    chmod +x kubectl && \
    mv kubectl /usr/local/bin/ && \

    # Install Trivy
    wget https://github.com/aquasecurity/trivy/releases/download/v0.56.0/trivy_0.56.0_Linux-64bit.deb && \
    dpkg -i trivy_0.56.0_Linux-64bit.deb && \
    rm trivy_0.56.0_Linux-64bit.deb && \

    # Install AWS CLI
    curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
    unzip awscliv2.zip && \
    ./aws/install && \
    rm -rf awscliv2.zip aws/ && \

    # Create a virtual environment for Python and Ansible
    python3 -m venv /opt/ansible_venv && \
    /opt/ansible_venv/bin/pip install --upgrade pip && \
    /opt/ansible_venv/bin/pip install ansible && \

    # Create symlinks to make Ansible easily accessible
    ln -s /opt/ansible_venv/bin/ansible /usr/local/bin/ansible && \
    ln -s /opt/ansible_venv/bin/ansible-playbook /usr/local/bin/ansible-playbook && \

    # Clean up
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

USER jenkins

Build and run this universal Jenkins image to mount Jenkins home directory and Docker socket from the host to the container, also add Docker group to the container so Jenkins can run Docker commands inside the container without needing root privileges.

docker build -t jenkins-all .

docker run -d --name jenkins -p 8080:8080 -p 50000:50000 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /var/jenkins_home:/var/jenkins_home \
--group-add $(getent group docker | cut -d: -f3) \ 
jenkins-all
  • Universal Jenkins CI pipeline design

After installing a list of plugins and configuring all credentials and the GitHub webhook, the CI pipeline is designed below and can be triggered by a Git push event and will run automatically:

  • Enable multi-language artifact build support.
  • Integrate testing of Java versions.
  • Enable security checks for static code analysis and Docker image scanning.
  • Implement advanced Jenkins pipeline structuring using try-catch, if-else, timeouts, environment variables, post actions and error handling.
// Define the detectJavaVersion function outside of the pipeline block
def detectJavaVersion() {
    def javaVersionOutput = sh(script: 'java -version 2>&1', returnStatus: false, returnStdout: true).trim()
    def javaVersionMatch = javaVersionOutput =~ /openjdk version "(\d+\.\d+)/

    if (javaVersionMatch) {
        def javaVersion = javaVersionMatch[0][1]

        if (javaVersion.startsWith("1.8")) {
            return '8'
        } else if (javaVersion.startsWith("11")) {
            return '11'
        } else if (javaVersion.startsWith("17")) {
            return '17'
        } else {
            error("Unsupported Java version detected: ${javaVersion}")
        }
    } else {
        error("Java version information not found in output.")
    }
}
pipeline {
    agent any
    environment {
        REGISTRY_URL = 'https://index.docker.io/v1/'
        IMAGE_NAME = "zackz001/jenkins"
        IMAGE_TAG = "${env.BUILD_NUMBER}"
        LATEST_TAG = "latest"
        TRIVY_OUTPUT = "trivy-report.txt"
        EMAIL_RECIPIENT = "[email protected]"
        GIT_REPO_URL = 'https://github.com/ZackZhouHB/zack-gitops-project.git'  // Git repository URL
        GIT_BRANCH = 'jenkins'  // Git branch
        DOCKERHUB_CREDENTIALS_ID = 'dockerhub' // Docker Hub credentials
        SONAR_TOKEN = 'sonar'  // Fetch Sonar token securely
        SNYK_INSTALLATION = 'snyk' // Replace with your Snyk installation
        SNYK_TOKEN = 'snyktoken'  // Fetch Snyk token securely
    }
    stages {
        stage('Clean Workspace') {
            steps {
                cleanWs()
            }
        }   
        stage('Checkout Code') {
            steps {
                git branch: "${GIT_BRANCH}",
                    credentialsId: 'gittoken',
                    url: "${GIT_REPO_URL}"
            }   
        }
        stage('Detect and Set Java') {
            steps {
                script {
                    try {
                        def javaVersion = detectJavaVersion()  // Detect the Java version, e.g., "17"
                        def javaToolName = "Java_${javaVersion}"  // Expected tool name

                        // Try to set the Java version; fallback if the specific version isn't found
                        try {
                            tool name: javaToolName, type: 'jdk'
                            echo "Using Java version ${javaVersion}."
                        } catch (Exception toolError) {
                            echo "No JDK named ${javaToolName} found. Using default JDK."
                        }

                        // Verify Java version, regardless of whether the specific version was found
                        sh 'java --version'

                    } catch (Exception e) {
                        echo "Error during Java version detection: ${e.message}"
                        // Continue pipeline even if Java detection fails
                    }
                }
            }
        }
        
        stage('snyk_analysis') {
            steps {
                script {
                    echo 'Running Snyk security analysis...'
                    timeout(time: 5, unit: 'MINUTES') {  // Adjust the timeout value as necessary
                        try {
                            snykSecurity(
                                snykInstallation: SNYK_INSTALLATION,
                                snykTokenId: SNYK_TOKEN,
                                failOnIssues: false,
                                monitorProjectOnBuild: true,
                                additionalArguments: '--severity-threshold=low'
                            )
                       } catch (Exception e) {
                            currentBuild.result = 'FAILURE'
                            error("Error during snyk_analysis: ${e.message}")
                        }
                    }
                }
            }
        }

        
        stage('Frontend Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('package.json')) {
                            sh 'npm install --force'
                            sh 'npm test'
                        } else {
                            echo 'No package.json found, skipping Frontend build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during Frontend build and test: ${e.message}")
                    }
                }
            }
        }

        stage('Java Spring Boot Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('pom.xml')) {
                            sh 'mvn clean package'
                            sh 'mvn test'
                        } else {
                            echo 'No pom.xml found, skipping Java Spring Boot build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during Java Spring Boot build and test: ${e.message}")
                    }
                }
            }
        }

        stage('.NET Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('YourSolution.sln')) {
                            sh 'dotnet build'
                            sh 'dotnet test'
                        } else {
                            echo 'No YourSolution.sln found, skipping .NET build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during .NET build and test: ${e.message}")
                    }
                }
            }
        }

        stage('PHP Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('composer.json')) {
                            sh 'composer install'
                            sh 'phpunit'
                        } else {
                            echo 'No composer.json found, skipping PHP build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during PHP build and test: ${e.message}")
                    }
                }
            }
        }

        stage('iOS Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('YourProject.xcodeproj')) {
                            xcodebuild(buildDir: 'build', scheme: 'YourScheme')
                        } else {
                            echo 'No YourProject.xcodeproj found, skipping iOS build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during iOS build and test: ${e.message}")
                    }
                }
            }
        }

        stage('Android Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('build.gradle')) {
                            sh './gradlew build'
                            sh './gradlew test'
                        } else {
                            echo 'No build.gradle found, skipping Android build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during Android build and test: ${e.message}")
                    }
                }
            }
        }

        stage('Ruby on Rails Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('Gemfile.lock')) {
                            sh 'bundle install'
                            sh 'bundle exec rake db:migrate'
                            sh 'bundle exec rails test'
                        } else {
                            echo 'No Gemfile.lock found, skipping Ruby on Rails build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during Ruby on Rails build and test: ${e.message}")
                    }
                }
            }
        }

        stage('Flask Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('app.py')) {
                            sh 'pip install -r requirements.txt'
                            sh 'python -m unittest discover'
                        } else {
                            echo 'No app.py found, skipping Flask build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during Flask build and test: ${e.message}")
                    }
                }
            }
        }

        stage('Django Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('manage.py')) {
                            sh 'pip install -r requirements.txt'
                            sh 'python manage.py migrate'
                            sh 'python manage.py test'
                        } else {
                            echo 'No manage.py found, skipping Django build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during Django build and test: ${e.message}")
                    }
                }
            }
        }

        stage('Rust Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('Cargo.toml')) {
                            env.RUST_BACKTRACE = 'full'
                            sh 'cargo build'
                            sh 'cargo test'
                        } else {
                            echo "No Cargo.toml file found. Skipping Rust build and test."
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during Rust build and test: ${e.message}")
                    }
                }
            }
        }

        stage('Ruby Sinatra Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('app.rb')) {
                            sh 'gem install bundler'
                            sh 'bundle install'
                            sh 'bundle exec rake test'
                        } else {
                            echo "No app.rb file found. Skipping Ruby Sinatra build and test."
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during Ruby Sinatra build and test: ${e.message}")
                    }
                }
            }
        }
        
        stage('Check and Build Docker Image') {
            steps {
                script {
                    try {
                        // Check if Docker is available
                        sh 'docker --version'
                        echo "Docker is installed. Proceeding to build the Docker image..."

                        // Build the Docker image from the 'zack_blog' folder
                        dockerImage = docker.build("${IMAGE_NAME}:${IMAGE_TAG}", "zack_blog/")
                    } catch (Exception e) {
                        // Handle the error if Docker is not available
                        error("Docker is not installed or accessible. Cannot proceed with the build.")
                    }
                }
            }
        }
        
        stage('Docker Image Scan') {
            steps {
                // Use Trivy to scan the built Docker image
                sh "trivy image --severity HIGH,CRITICAL ${IMAGE_NAME}:${IMAGE_TAG} > ${TRIVY_OUTPUT}"
            }
        }
        
        stage('Push Docker Image to DockerHub') {
            steps {
                script {
                    docker.withRegistry("${REGISTRY_URL}", "${DOCKERHUB_CREDENTIALS_ID}") {
                        dockerImage.push("${IMAGE_TAG}")
                        dockerImage.push("${LATEST_TAG}") // Push 'latest' tag
                    }
                }
            }
        }
        
        stage('Display Trivy Scan Results') {
            steps {
                script {
                    // Display the contents of the Trivy report
                    def scanReport = readFile("${TRIVY_OUTPUT}")
                    echo "Trivy Scan Report:\n${scanReport}"
                }
            }
        }
        
    }
    
    post {
        success {
            script {
                def scanReport = readFile("${TRIVY_OUTPUT}")
                emailext(
                    to: "${EMAIL_RECIPIENT}",
                    subject: "CI Pipeline Success: Build ${IMAGE_TAG}",
                    body: """
                    The pipeline has successfully completed.

                    Docker image ${IMAGE_NAME}:${IMAGE_TAG} has been built and pushed to DockerHub.

                    Trivy Scan Report:
                    ${scanReport}
                    """
                )
            }
        }
        failure {
            emailext(
                to: "${EMAIL_RECIPIENT}",
                subject: "CI Pipeline Failed: Build ${IMAGE_TAG}",
                body: """
                The pipeline has failed at some stage.

                Please check the Jenkins console logs for more details.
                """
            )
        }
    }
}

Pipeline test and debug

After thorough testing and validation, the CI pipeline finally works like a charm.

image tooltip here Next, I will create a CD pipeline to integrate with Ansible, AWS, and Terraform to deploy the blog onto AWS EC2, ECS, and EKS.

Welcome to Zack's Blog

Join me for fun journey about ##AWS ##DevOps ##Kubenetes ##MLOps

  • Latest Posts