Effortless Kubernetes Jenkins Agent Configuration: Harnessing Shared Libraries

Β·

9 min read

Effortless Kubernetes Jenkins Agent Configuration: Harnessing Shared Libraries

Introduction πŸ“–

In Kubernetes (K8s) environments, Jenkins can serve as a crucial tool for automation in continuous integration and continuous delivery (CI/CD) pipelines where the Jenkins Manager is an always running pod and Jenkins Agents are ephemeral pods that are created and terminated dynamically based on job execution. However, managing Jenkins agents as pods in Kubernetes can be cumbersome, especially when dealing with multiple pipelines and changing agent requirements. Traditionally, each Jenkins pipeline would include its own Kubernetes YAML manifest for agent configuration, violating the DRY (Don't Repeat Yourself) principle and making adapting to external changes a significant overhead.

In this article, we'll explore how Jenkins Shared Libraries offer a solution to centralize and simplify the configuration of Jenkins agents in Kubernetes.

Prerequisites πŸ“š

Before implementing Jenkins Shared Libraries for centralized Jenkins agent configuration in Kubernetes, ensure you have the following:

  1. Understanding of Jenkins and Kubernetes: Familiarity with Jenkins for CI/CD pipelines and Kubernetes for container orchestration is essential.
  2. Access to a Kubernetes Cluster: You'll need access to a Kubernetes cluster where Jenkins is deployed as a master.
  3. The Jenkins Kubernetes Plugin: Ensure the Jenkins Kubernetes Plugin is installed on your Jenkins master for dynamic agent provisioning.

The Problem ⁉️

When deploying Jenkins on Kubernetes, the traditional approach involved writing Kubernetes YAML manifests for Jenkins agents directly within each Jenkinsfile. This leads to duplication of effort and makes it challenging to manage changes across multiple pipelines. For instance, updates necessitated by external factors like projects specification changes and Kubernetes version upgrades adds further complexity and maintenance overhead. [case in point: k8s dropping the support for the Dockershim in the update from version 1.23 to 1.24 and needing to build docker images in the pipeline inside a dind (Docker in Docker) container]

Solution: Leveraging Jenkins Shared Libraries πŸ’‘

Setting Up Shared Libraries πŸš€

To streamline Jenkins agent configuration, we leverage Jenkins Shared Libraries. Here's a step-by-step guide:

  1. Create a Shared Library Repository: Begin by setting up a Git repository to host your Jenkins Shared Libraries. The repository structure should include directories for Groovy scripts (vars) and pipeline resources (resources), along with a README.md file for documentation.

    <Shared_Library_Root>
    |-- vars                           // Directory for global variables
    |   |-- getAgentYaml.groovy        // Groovy script for retrieving agent YAML
    |   `-- getDefaultContainer.groovy // Groovy script for default container configuration
    |-- resources                      // Directory for non-Groovy files
    |   |-- default-agent.yaml         // Kubernetes YAML manifest for agent configuration
    |   `-- default-container.txt      // Default container specifications
    `-- README.md                      // Documentation for your shared library
    
  2. Define Groovy Scripts: Within the vars directory, define Groovy scripts encapsulating functions to retrieve the Kubernetes YAML for agent configuration dynamically. For instance, create scripts like getAgentYaml.groovy and getDefaultContainer.groovy to retrieve YAML manifests and default container configurations, respectively.

    • File: getAgentYaml.groovy

      String call() {
          String filecontents = libraryResource 'default-agent.yaml'
          return filecontents
      }
      
    • File: getDefaultContainer.groovy

        String call() {
             String filecontents = libraryResource 'default-container.txt'
             return filecontents
        }
      
  3. Organize Resources: Populate the resources directory with the necessary asset files, such as default-agent.yaml for agent configuration and default-container.txt for default container specifications. Notice the use of the libraryResource step to retrieve the contents of these files within the Groovy scripts.

    • Example for contents of the file default-agent.yaml

      
      apiVersion: v1
      kind: Pod
      spec:
        serviceAccountName: jenkins-agent
        containers:
          - name: shell
            image: jenkins/inbound-agent:latest
            command:
              - sleep
            args:
              - infinity
          - name: docker-dind
            image: docker:dind
            securityContext:
              privileged: true
            volumeMounts:
              - name: dind-storage
                mountPath: /var/lib/docker
        volumes:
          - name: dind-storage
            hostPath:
              path: /var/lib/docker
              type: DirectoryOrCreate
      
    • Example for contents of the file default-container.txt

      shell
      
    • Notes:

      • To improve efficiency, I opted for a hostPath volume for the docker-dind container. This allows caching of pulled images when building docker files, enabling faster subsequent pipeline runs on the same node.
      • You should update the source images to either images hosted in a local network registry if you're running on-prem or images hosted inside your cloud provider's container registry if you're on the cloud. This to done to reduce the time it takes for pipelines to start.
      • Employ specific image tags instead of latest to ensure stability.
      • You could (and should) enhance security by replacing dind (Docker in Docker) with Kaniko or Buildah to avoid the necessity for privileged containers.
      • You have to customize the service account name to match your Jenkins agent service account for proper authentication.
      • The file default-container.txt contains the name of the default container to be used for the agent pod. This can be overridden in the Jenkinsfile if needed.

Integrating Shared Libraries with Jenkins πŸ”—

  1. Configure Global Pipeline Libraries: In the Jenkins web interface, navigate to "Manage Jenkins" > "Configure System" > "Global Pipeline Libraries." Add your Shared Library repository URL and configure loading options, including implicit loading and version control settings. Shared Libraries in Jenkins UI
  2. Define Retrieval Method: Configure the library retrieval method as modern SCM, and set up the repository details accordingly. This ensures Jenkins fetches the latest library version from the specified branch or commit. SCM for Shared Libraries
  3. Usage in Jenkins Pipelines: Utilize the Shared Libraries in your Jenkins pipelines by importing them at the beginning of the script. To invoke the call() function you defined earlier, You can just use the file name without the extension. For example, getAgentYaml() and getDefaultContainer() since call() is the default function that gets called when no function is specified.

  4. Example Pipeline Usage

    @Library("jenkins-agent@master") _   // Import the Shared Library
    pipeline {
       agent {
           kubernetes {
               yaml getAgentYaml()                   // Retrieve agent YAML manifest
               defaultContainer getDefaultContainer() // Retrieve default container configuration
           }
       }
       stages {
           // Define pipeline stages
           ...
       }
    }
    

And that's it! You've successfully integrated Jenkins Shared Libraries to centralize and simplify Jenkins agent configuration in Kubernetes.

Old Agent DefinitionNew Agent Definition
Jenkinsfile without Shared LibrariesJenkinsfile with Shared Libraries

Now our Jenkinsfile is much cleaner and easier to maintain. We can also easily update the agent configuration across all pipelines by making changes to the Shared Library.

Future Enhancements: Super Charge Jenkins Shared Libraries ⚑

As you gain proficiency with Jenkins Shared Libraries, consider extending their capabilities to further streamline pipeline configurations and enhance reusability. Here are some future enhancements you can implement:

1. Passing Parameters for Different Agent Configurations

Implement parameter passing to dynamically retrieve agent configurations based on the provided parameters. This flexible approach enables customization of agent settings to align with various technology stacks or team requirements.

  • file vars/agent.groovy

    /**
     * Retrieves the Kubernetes YAML manifest for a Jenkins agent based on the specified parameters.
     * 
     * @param stack The technology stack for which the agent configuration is needed.
     * @return The Kubernetes YAML manifest for the Jenkins agent.
     */
    def getCustomAgentYaml(String stack) {
        switch(stack) {
            case "python":
                return libraryResource 'python-agent.yaml'
            case "nodejs":
                return libraryResource 'nodejs-agent.yaml'
            default:
                return libraryResource 'default-agent.yaml'
        }
    }
    

In this example, Depending on the stack specified (e.g., "python" or "nodejs"), the function retrieves the corresponding Kubernetes YAML manifest for the Jenkins agent.

2. More Groovy Functions for Common Functionality

Encapsulate common build, deployment, and infrastructure tasks into parameterized Groovy functions within your Shared Libraries. These functions can accept parameters to customize their behavior and promote consistency across pipelines.

Some examples when running on AWS are:

  • file vars/deploy.groovy

     /**
      * Updates the code for an AWS Lambda function.
      * 
      * @param functionName The name of the Lambda function to update.
      * @param codePath The path to the code package to deploy.
      * @param region The AWS region where the Lambda function is deployed.
      */
     def updateLambdaCode(String functionName, String codePath, String region) {
         // Implement logic to update Lambda code using AWS SDK or CLI
     }
    
     /**
      * Pushes a Docker image to Amazon Elastic Container Registry (ECR).
      * 
      * @param registryUrl The name of the AWS ECR that hosts your repository.
      * @param repositoryName The name of repository that hosts the Docker image.
      * @param tag The tag of the Docker image.
      * @param region The AWS region where the ECR repository is located.
      */
     def pushToECR(String registryUrl, String repositoryName, String tag, String region) {
         // Implement logic to push Docker image to ECR using AWS CLI or SDK
     }
    
     /**
      * Applies Kubernetes manifests to a Kubernetes cluster.
      * 
      * @param manifestsPath The path to the directory containing Kubernetes manifests.
      */
     def applyKubernetesManifests(String manifestsPath) {
         // Implement logic to apply Kubernetes manifests using kubectl or k8s client libraries
    

These parameterized Groovy functions enable you to automate various aspects of your CI/CD workflows, such as updating Lambda code, pushing Docker images to ECR, and applying Kubernetes manifests. By integrating these functions into your Jenkins pipelines, you can enhance reusability, promote consistency, and streamline pipeline configurations effectively.

Important Note πŸ“£

Since we are not defining the default call() function in these previous two use cases we would call this function inside our pipelines a little differently, We have to append the function name to the file name inside the vars directory.

  • Example of how to call these functions inside the Jenkinsfile:

    agent.getCustomAgentYaml("python")
    
    deploy.updateLambdaCode("functionName", "codePath", "region")
    
    deploy.pushToECR("registryUrl", "repositoryName", "tag", "region")
    

where agent and deploy are the names of the groovy files inside the var directory and getCustomAgentYaml, updateLambdaCode, pushToECR are the names of the functions inside of these groovy files.

With Great Power Comes Great Responsibility πŸ¦Έβ€β™‚οΈ

As we venture deeper into the realm of Jenkins Shared Libraries, let us not forget the age-old wisdom: with great power comes great responsibility. While we wield the tools of automation and configuration with finesse, it's essential to tread carefully in the labyrinth of code changes.

Imagine a world where a single line of code can send your pipelines into a frenzy, a digital domino effect of chaos and confusion. But fear not, dear adventurer, for Jenkins offers us a a way to test our Shared Libraries changes before unleashing them into the wild, The "Allow default version to be overridden" checkbox

So, when the urge to tweak and tinker strikes, why not venture forth into the uncharted territory of a new branch? Give it a whimsical name like beta or sandbox, where you can conduct experiments like a mad scientist in a laboratory. Experiment, iterate, and test to your heart's content, knowing that the sanctity of the master branch remains untarnished.

Beta Jenkins Library

And when your code changes have been polished to perfection, when every bug has been squashed and every edge case accounted for, then, my friends, shall you proudly merge your triumphant code back into the master branch.

So, fellow Jenkins journeyers, let's embrace the power of Shared Libraries with caution and cunning. After all, in the world of continuous integration, a little testing goes a long way, just like double-checking your parachute before jumping out of a plane.

Conclusion 🏁

In conclusion, Jenkins Shared Libraries provide a robust mechanism for extending and customizing Jenkins pipelines within Kubernetes environments. By centralizing and abstracting common functionality and adhering to the DRY principle, teams can streamline agent configuration, promote maintainability, and reduce duplication of effort. This approach fosters reusability and adaptability to evolving requirements and technologies, ultimately enabling teams to build tailored CI/CD solutions that are both robust and flexible. Nonetheless, it is important to tread carefully in the realm of Jenkins Shared Libraries, ensuring thorough testing and cautious implementation to avoid unintended consequences.

Β