Automating Your CI/CD Workflow with Jenkins and GitHub Webhooks

How to Set Up a Jenkins Job to Automatically Build and Deploy on GitHub Push Events

Automating Your CI/CD Workflow with Jenkins and GitHub Webhooks

As software development teams grow, so does the need for reliable and efficient continuous integration and deployment (CI/CD) workflows. Jenkins is a popular tool for automating the building, testing, and deployment of software projects. GitHub, on the other hand, is a widely used platform for hosting and managing source code repositories. Combining Jenkins with GitHub Webhooks can help streamline your CI/CD workflow by automatically triggering builds and deployments whenever changes are pushed to your repository. In this blog, we'll explore how to set up Jenkins to use GitHub Webhooks and show you how to trigger a build whenever a push event occurs. With this integration, you can save time and reduce errors by automating your CI/CD process.

We will do everything from scratch from creating an EC2 instance with the help of Terraform to installing and configuring Jenkins and docker on the server using Ansible.

This CI/CD pipeline will first build a docker image out of a dockerfile and then push it to the docker hub registry.

Prerequisites:-

  • AWS Account

  • Terraform

  • Ansible is installed on the local system.

Note: I have two repositories for this project one for creating the entire Jenkins server and one for the project from which we are going to create a Docker Image.

Creating Jenkins server using Terraform -

First, create main.tf file in the project directory

provider "aws" {
  region = "ap-south-1"
}

variable "vpc-cidr-block" {}
variable "subnet-cidr-block" {}
variable "availability_zone" {}
variable "env_prefix" {}
variable "ip-address" {}
variable "instance-type" {}
variable "public-key-path" {}

resource "aws_vpc" "myapp-vpc" {
  cidr_block = var.vpc-cidr-block
  tags = {
    Name = "${var.env_prefix}-vpc"
  }
}

resource "aws_subnet" "myapp-subnet" {
  vpc_id            = aws_vpc.myapp-vpc.id
  cidr_block        = var.subnet-cidr-block
  availability_zone = var.availability_zone
  tags = {
    Name = "${var.env_prefix}-subnet"
  }
}

resource "aws_internet_gateway" "myapp-internet-gateway" {
  vpc_id = aws_vpc.myapp-vpc.id
  tags = {
    Name = "${var.env_prefix}-internet-gateway"
  }
}

resource "aws_default_route_table" "main-route-table" {
  default_route_table_id = aws_vpc.myapp-vpc.default_route_table_id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.myapp-internet-gateway.id
  }
  tags = {
    Name = "${var.env_prefix}-main-route-table"
  }
}

resource "aws_security_group" "Jenkins-security-group" {
  name   = "Jenkins-security-group"
  vpc_id = aws_vpc.myapp-vpc.id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ip-address]
  }

  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port       = 0
    to_port         = 0
    protocol        = "-1"
    cidr_blocks     = ["0.0.0.0/0"]
    prefix_list_ids = []
  }

  tags = {
    Name = "Jenkins-${var.env_prefix}-security-group"
  }
}

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }
  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

output "aws-ami-id" {
  value = data.aws_ami.ubuntu.id
}

output "ec-2-public-ip" {
  value = aws_instance.myapp-server.public_ip
}

resource "aws_key_pair" "ssh-key" {
  key_name   = "server-key"
  public_key = file(var.public-key-path)
}

resource "aws_instance" "myapp-server" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance-type

  subnet_id                   = aws_subnet.myapp-subnet.id
  vpc_security_group_ids      = [aws_security_group.Jenkins-security-group.id]
  availability_zone           = var.availability_zone
  associate_public_ip_address = true
  key_name                    = aws_key_pair.ssh-key.key_name

  # user_data = file("entry-script.sh")     

  tags = {
    Name = "Jenkins-${var.env_prefix}-server"
  }
}

resource "null_resource" "update_hosts_file" {
  provisioner "local-exec" {
    command = "truncate -s 0 hosts && echo -n '${aws_instance.myapp-server.public_ip} ansible_user=ubuntu' >> hosts"
  }
}

I have already explained Infrastructure provisioning with terraform in this Blog.

Now apply these changes using

terraform apply -auto-approve

Configuring Jenkins Server using Ansible -

Create jenkins.yaml file inside the project folder

---
- name: Install Jenkins on Ubuntu Server
  hosts: all
  become: yes

  tasks:
  - name: Update apt cache
    apt:
      update_cache: yes

  - name: Install OpenJDK 11
    apt:
      name: openjdk-11-jdk
      state: present

  - name: Add Jenkins APT Key
    apt_key:
      url: https://pkg.jenkins.io/debian-stable/jenkins.io.key
      state: present

  - name: Add Jenkins APT Repository
    apt_repository:
      repo: deb https://pkg.jenkins.io/debian-stable binary/
      state: present

  - name: Install Jenkins
    apt:
      name: jenkins
      state: present

  - name: Start Jenkins Service
    service:
      name: jenkins
      state: started

First, we install Java JDK which is necessary for Jenkins. Then we add the official repository of Jenkins and install Jenkins on the instance.

---
- name: Install and configure Docker on Ubuntu
  hosts: all
  become: yes
  tasks:
  - name: Update package manager
    apt:
      update_cache: yes

  - name: Add Docker's official GPG key
    apt_key:
      url: https://download.docker.com/linux/ubuntu/gpg
      state: present

  - name: Add Docker's official repository
    apt_repository:
      repo: deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable
      state: present

  - name: Install pip
    apt:
      name: python3-pip
      state: present

  - name: Install docker module for python
    pip: 
      name: docker

  - name: Install Docker CE
    apt:
      name: docker-ce
      state: present

  - name: Add current user to the Docker group
    shell: sudo usermod -a -G docker jenkins

  - name: Restart Jenkins
    service:
        name: jenkins
        state: restarted

Here also add the first official repository of Docker. Then we install python for the installation of docker. After installation, we must add a Jenkins user to the docker group so that we can execute docker commands.

Now let’s apply our playbooks, For that, we need to have a host file that contains the IP address of our server. That we have already done in terraform provisioning, so let’s apply changes

ansible-playbook -i hosts Ansible/jenkins.yaml
ansible-playbook -i hosts Ansible/docker.yaml

Here -i indicates inventory which contains the host address followed by a path to the yaml file.

Setting up Jenkins Server:-

After the successful execution of ansible, commands let’s access our jenkins server by using URL -

http://<External-IP-Address>:8080

This screen shows up -

For the Jenkins password ssh into your Jenkins server using

ssh ubuntu@<External-IP-Address>

and run sudo cat /var/lib/jenkins/secrets/initialAdminPassword

Copy the output string and paste it in as a password. Now follow the normal Jenkins installation process.

GitHub Integration:-

For Github integration we first need to add our GitHub server to Jenkins, for that go to GitHub -> Settings -> Developer Settings -> Personal Access Token (Classic)

Click on generate a new token, give the name as Jenkins and copy that token as we need it in the future.

Now go to Jenkins server in Manage Jenkins -> Configure System

Scroll down to the GitHub section and click on Add Github Server.

Click on Add below the Credentials field. Select the kind as Secret text.

Enter the copied token in the Secret field and type Jenkins in the ID field and click on add. Now click on credentials you will see the option as Jenkins which we just created. You can also test the connection.

GitHub Webhook:-

To create and configure the Github webhook go to settings in your project repository. Click on the Webhooks section and click on add webhook.

Enter payload Url as - http://<External-IP>:8080/github-webhook/

Click on add webhook.

Note: Make sure that GitHub shows a green check mark at the left side of your webhook that confirms that the URL is correct and working as expected.

Jenkins CI Pipeline:-

Create a new project in Jenkins, give it any name you want and select pipeline. Select the GitHub hook trigger for GITScm polling in the build trigger section.

In the pipeline section select Pipeline script from SCM and then select Git in the SCM section. Enter your repository URL in my case it’s going to be https://github.com/PranavMasekar/DevOps-Project/tree/DevOps.

Specify the branch and click on Save.

Now let’s prepare our Jenkinsfile to go to your project and create a Jenkins file -

pipeline {
  agent any
  environment {
    DOCKER_HUB_CREDENTIALS = credentials('docker-hub')
    DOCKER_IMAGE_NAME = 'pranav18vk/go-restro'
    DOCKER_IMAGE_TAG = "latest"
    DOCKER_REGISTRY_URL = 'https://index.docker.io/v1/'
  }
  stages {
    stage('Build Docker Image') {
      steps {
        script {
          sh "docker build -t ${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG} ."
        }
      }
    }
    stage('Push Docker Image') {
      steps {
        script {
          withCredentials([usernamePassword(credentialsId: 'docker-hub', usernameVariable: 'DOCKER_HUB_CREDENTIALS_USR', passwordVariable: 'DOCKER_HUB_CREDENTIALS_PSW')]) {
            sh "echo ${DOCKER_HUB_CREDENTIALS_PSW} | docker login -u ${DOCKER_HUB_CREDENTIALS_USR} --password-stdin ${DOCKER_REGISTRY_URL}"
            sh "docker push ${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}"
          }
        }
      }
    }
  }
}

To push images to the docker hub we also need to set up the docker hub credentials in Jenkins. For that go to

Manage Jenkins -> Manage Credentials -> Click on global

Click on Add Credentials, and select Username with password in the kind section. Add the docker hub username and password in respective fields and give the ID as docker-hub.

Now commit your changes and push them to the GitHub repository and check whether the job has been triggered or not.

🤔🤔 Job has not been triggered ??

That’s because Jenkins doesn’t register the first triggered job via webhook. The first job should be triggered manually so click on the play button on the Jenkins dashboard page. Check whether the pipeline is working correctly or not.

After the first manual build do another push to the repository and this time Jenkins job will be triggered.

Congratulations 🥳🥳 on completing this hands-on tutorial on setting up Jenkins with GitHub Webhooks to trigger a job on push events! By following the steps outlined in this blog, you have learned how to automate your CI/CD workflow and streamline your software development process. Keep exploring the many powerful features of Jenkins and GitHub to continue improving your development workflow, and happy coding!

GitHub link for Jenkins Server.

GitHub link for Project.

Did you find this article valuable?

Support Pranav Masekar by becoming a sponsor. Any amount is appreciated!