Automation lies at the heart of DevOps. Various automation tools and techniques have truly enabled the concept of continuous integration and continuous delivery. These tools have evolved over the years rapidly but one name that seems to be here forever is Jenkins.
We will neither discuss introductory concepts of CI-CD in this post nor waste time showing Jenkins installation steps. If you are new to Jenkins, you can check out the Offical Installation Docs to get started with Jenkins. Therefore, the purpose of this post is to discuss how to setup Jenkins controller-agent architecture (aka master-slave architecture) and address some of the issues that arise when doing it. This is because, the process can be tedious and if you haven't done it in a while, you may end up wasting a few hours.
The controller(master) node is Jenkin's brain, it's where the Jenkins application runs. If we do too much work on the controller node (or it crashes), the entire application may become unavailable. Therefore, we want the master to be as available as possible. This can be done by delegating work to agent nodes (slave nodes). So in a Jenkins Controller-Agent architecture, the jobs are scheduled and assigned to agents by the controller. The controller also keeps track of whether the slaves are online, retrieve their responses of the build results, and outputs the build results on the console. Thus, the master node is more available and hence the overall performance of our Jenkins server is improved using this design.
Another advantage of this architecture is that we can only install a minimum set of tools on the controller node and we can install the heavier tools (required by the jobs) on the agent nodes. This keeps the controller lightweight and also allows us to organize our jobs based on the agents that should execute them. In the above example, we have a Jenkins controller along with 4 agents. Each of the agents can serve a specific purpose.
- Similarly, if we need to build some .NET applications we can setup a Jenkins agent with a windows host and restrict these jobs to be executed on the far right.
- Furthermore, we can improve performance by balancing the loads based on our system's requirements. Let's say, we are setting up CI-CD of a system that has hundreds of microservices among which, there are twice as many services written using python based stack as any other stack. In this situation, we can setup two Jenkins agents with python based tools installed and the Jenkins controller can balance the load between these two agents.
We can use the official jenkins docker container. Here's an example docker-compose file that you can use.
Now, we need to spin up the container and we need to install the required tools for the Jenkins controller node. We can write a simple bash script to achieve that.
set -o nounset
echo "JENKINS HOME: $JENKINS_HOME"
CONTAINER_NAME=$JENKINS_CONTAINER_NAME JENKINS_HOME=$JENKINS_HOME docker-compose -f docker-compose-controller.yaml up --build -d
# Here we have just installed git for our controller node, install as many tools as you require
docker exec $JENKINS_CONTAINER_NAME bash -c "apt-get update -y -q && apt-get upgrade -y -q && apt-get install -y -q git"
- Jenkins uses apache jetty which uses port 8080 by default, We mapped our host machine's port 50001 with the container's 8080. So, entering http://host:50001 should take you to the Jenkins web dashboard.
- Check container logs for the first time admin password and create a new admin user.
We can now setup our agent(s). Since, our Jenkins controller will communicate with the agents using SSH, we need to generate the SSH keys. In this case, the Jenkins master node will act as the SSH client and the agent(s) will act as SSH servers. So, we need to set it up accordingly.
- Generate Keys
ssh-keygen -t rsa -f jenkins_agent_1
- Goto Jenkins Dashboard > Manage Jenkins > Manage Credentials > Add 'System' scoped Credential for enabling SSH into a Jenkins Agent
System Credential vs Global Credential
System: Only available on Jenkins server (not visible by jenkins job)
Global: Accessible everywhere including jenkins job
Fill up the form with the appropriate values. Here's an example,
Username: jenkins # we want to ssh into the agent as 'jenkins' user which already exists by default in the jenkins-agent container we will be using
ID: An Unique ID for the credential that can be used to refer to the credential
Private Key: SSH Private Key file contents (e.g: jenkins_agent_1)
We can use the official jenkins-ssh-agent docker container. Here's an example docker-compose file that you can use.
Notice that, we must set the environment variable
JENKINS_AGENT_SSH_PUBKEY which in this case we are doing from a bash variable. We also need to install the required tools in our Jenkins agent. We can achieve all that using a simple bash script like the one below,
set -o nounset
CONTAINER_NAME=$JENKINS_CONTAINER_NAME JENKINS_AGENT_SSH_PUBKEY=$JENKINS_AGENT_SSH_PUBKEY docker-compose -f docker-compose-agent.yaml up --build -d
# Here we have just installed tools that help us create a python virtual environment for our agent node, install as many tools as you require
docker exec $JENKINS_CONTAINER_NAME bash -c "apt-get update -y -q && apt-get upgrade -y -q && apt-get install -y -q git python3 python3-venv"
Goto Jenkins Dashboard > Manage Jenkins > Manage Nodes and Clouds > New Node
Fill up the form using the appropriate values. Here's an example,
Labels: linux, python # Space separated values, Can be useful to restrict jobs to run on a particular agent
Usage: Use this node as much as possible
Launch Method: Launch agents via SSH
Host: jenkins_agent # Agent's Hostname or IP to connect. (docker-compose service name if controller and agent is on the same machine)
Credentials: Select the Credential created in Step 2
HostKeyVerificationStrategy: Non verifying Verification Strategy
Launch Method > Advanced
There are some other options you might wanna look into, but the ones i discussed should help you get started.
We can create as many agents as we require using the process discussed above.
Now, our agent should be discovered by the controller and we can start delegating our jobs to the agents. We can restrict a job to run on a particular agent by using the labels we assigned when creating an agent.
That's done :D. Let me know, if missed anything. You can also find the source files here: ashiqursuperfly/Jenkins-Controller-Agent-Setup