# Jenkins CI CD Project 1

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1713769393330/39023508-9959-4026-bede-7da66f39ee5f.png align="center")

# Objectives

* Build the code using Jenkins
    
* Integrate Sonar
    
* Deploy the generated artifact to two EC2 instances.
    
* The two EC2 instances should have Apache Tomcat installed using Ansible.
    
* Load balance the traffic to two EC2 servers using load balancer.
    
* Once the application is stable, try implementing the blue-green.
    
    * Make a change in the codebase and deploy the updated code to ONLY one of the EC2 instances.
        
    * Upon deployment, your load balancer should serve from both the EC2 instances and show your updated page when refreshed (randomly)
        

# Github Repo

[https://github.com/melvincv/java-basic-practice-app](https://github.com/melvincv/java-basic-practice-app)

# Steps

## **Jenkins Setup**

Start an EC2 instance with the following parameters:

* name: jenkins-aio
    
* ami: Ubuntu 22.04 LTS 64 bit
    
* type: t3.small
    
* security group: open ports 80, 443, 22
    
* spot instance: true
    
* user data: (check and modify shell script variables first)
    
* [https://github.com/melvincv/java-basic-practice-app/blob/main/scripts/install-jenkins.sh](https://github.com/melvincv/java-basic-practice-app/blob/main/scripts/install-jenkins.sh)
    

The script installs openjdk 17, maven 3.9.6, Jenkins from the official repos, Docker, Ansible and Caddy reverse proxy.

Be sure to set the DNS A record to the IP address of your Jenkins instance asap.

SSH to the jenkins-aio instance, Check for instance init progress

```bash
tail -f /var/log/cloud-init-output.log
Cloud-init v. 23.4.4-0ubuntu0~22.04.1 finished at Sat, 20 Apr 2024 09:33:31
```

and lookup your domain name. Confirm that it is pointing to your instance before starting the Caddy service.

```bash
nslookup jenkins.aws.melvincv.com
sudo systemctl start caddy
```

```bash
ubuntu@jenkins-aio:~$ sudo systemctl status caddy
● caddy.service - Caddy
     Loaded: loaded (/lib/systemd/system/caddy.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2024-04-19 05:57:00 UTC; 1 day 3h ago

{"level":"info","ts":1713506223.091408,"logger":"tls.obtain","msg":"certificate obtained successfully","identifier":"jenkin>
```

At this point, Jenkins will be available on your domain with Let's Encrypt SSL.

Eg: [https://jenkins.aws.melvincv.com/](https://jenkins.aws.melvincv.com/)

Fill in the Admin password:

```bash
sudo cat /var/lib/jenkins/secrets/initialAdminPassword
```

Install Suggested Plugins

Create an Admin User and Log In

### **Adding Tools**

Now, you may configure the Tools section.

Manage Jenkins &gt; Tools &gt; Add JDK

name: openjdk17

path: /usr/lib/jvm/java-1.17.0-openjdk-amd64

Get the path using this command:

```bash
ubuntu@jenkins-aio:~$ update-java-alternatives --list
java-1.17.0-openjdk-amd64      1711       /usr/lib/jvm/java-1.17.0-openjdk-amd64
```

![](https://lh7-us.googleusercontent.com/WJN4MvZjWXdYJRb90hajxL-2tLBf6AIy-fUh3PnC0aWfcDrpzXm6TiQhVK7FyAJhbQUR3DQ2SauCJVe5B0hWfb8T5OyY3SBUjur9HAtH9uFBOEE0_kFf1j9Ho-53yQyxpITpfPiEwvfKh3_HiacmKZQ align="left")

Manage Jenkins &gt; Tools &gt; Add Maven

name: maven3.9

path: /opt/apache-maven

Get the path using this command: (you get the Java path too)

```bash
ubuntu@jenkins-aio:~$ mvn -v
Apache Maven 3.9.6 (bc0240f3c744dd6b6ec2920b3cd08dcc295161ae)
Maven home: /opt/apache-maven
Java version: 17.0.10, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
```

### **Adding Plugins**

Add Plugins to extend the functionality of Jenkins.

Manage Jenkins &gt; Plugins &gt; Available

* sonarqube scanner
    
* ssh agent
    

### **Create a Pipeline Job**

Dashboard &gt; New item &gt; give a name &gt; Pipeline

![](https://lh7-us.googleusercontent.com/YKdEJagXn6SQ8gErmw3CJS4WYb8pejJiAYNaN56Ng8x2MF0YIUuD_eCstMpdwjJr63OX3JbahdNIRJbOhyP0CvaDk9B99yNVxbz4Mwgs0FIhhTZoxuIycj_XJ-79gkdVJr61nIfva8uJ8nHsJJusNx4 align="left")

Configure section will be opened.

Pipeline &gt; Pipeline script from SCM &gt; Git &gt; Enter the repo URL

Branch Specifier = \*/main

Save.

![](https://lh7-us.googleusercontent.com/yOHZPZpNWIdfIUQ23yLQlmBal_7CpLruVguFDjJyRP3nxgBAAmR_jEJHyw7FElciFQwyGxf09de1Ehnq5M0uIVQ6tdtC0bgxp2drwTgLUZ0QxxTo9OKfpMoRW_f18BfGMhVCn-AbS_p_q_wQRtVV9cI align="left")

![](https://lh7-us.googleusercontent.com/ZjyOEwP1D0mW_kFAe1MkglijpfbqzXMU-UZr76oyAF0vs-Iy6yck0vCSCXBqKhAOnJIYcUZEdUmm2IQ9f3Rn2ei4jkgFv9sbm7Pejvg2enAFyhBUcf25EOvnNFHEdrVZfLyn5N_sxM8utUXnlTXUMkg align="left")

## **Sonarqube Setup**

Start an EC2 instance with the following parameters:

* name: sonarqube
    
* ami: Ubuntu 22.04 LTS 64 bit
    
* type: t3.medium
    
* security group: open ports 80, 443, 22
    
* spot instance: true
    
* user data: (check and modify shell script variables first)
    
* [https://github.com/melvincv/java-basic-practice-app/blob/main/scripts/sonarqube/prereq.sh](https://github.com/melvincv/java-basic-practice-app/blob/main/scripts/sonarqube/prereq.sh)
    

The script configures kernel settings, installs Docker and Caddy reverse proxy.

Be sure to set the DNS A record to the IP address of your Sonarqube instance asap.

SSH to the sonarqube instance, Check for instance init progress

```bash
tail -f /var/log/cloud-init-output.log
Cloud-init v. 23.4.4-0ubuntu0~22.04.1 finished at Sat, 20 Apr 2024 09:33:31
```

and lookup your domain name. Confirm that it is pointing to your instance before starting the Caddy service.

```bash
nslookup jenkins.aws.melvincv.com
sudo systemctl start caddy
sudo systemctl status caddy
```

Check for the certificate issue, similar to Jenkins.

But Sonarqube is not ready yet and you will get an HTTP error 502 (Bad gateway). This is because the upstream server is not available - the server(s) behind the reverse proxy.

### **Install Sonarqube using Docker Compose**

Create a folder for the Docker files

```bash
mkdir sonar; cd sonar
```

Create a .env file with the PostgreSQL Password

```bash
cat > .env
POSTGRES_PASSWORD=xxxxxxxxxxxxxxxxxxxx
```

Create a compose.yml file

My compose file: [https://github.com/melvincv/java-basic-practice-app/blob/main/scripts/sonarqube/compose.yml](https://github.com/melvincv/java-basic-practice-app/blob/main/scripts/sonarqube/compose.yml)

Start Sonarqube using Compose:

```bash
docker compose pull
docker compose up -d
```

The Compose file starts 3 containers:

* sonarqube lts version
    
* postgres 12
    
* adminer (optional, for DB management)
    

```bash
$ docker compose ps
NAME                IMAGE                     COMMAND                  SERVICE     CREATED              STATUS              PORTS
sonar-adminer-1     adminer                   "entrypoint.sh php -..."   adminer     About a minute ago   Up About a minute   0.0.0.0:10000->8080/tcp, :::10000->8080/tcp
sonar-db-1          postgres:12               "docker-entrypoint.s..."   db          About a minute ago   Up About a minute   5432/tcp
sonar-sonarqube-1   sonarqube:lts-community   "/opt/sonarqube/dock..."   sonarqube   About a minute ago   Up About a minute   0.0.0.0:9000->9000/tcp, :::9000->9000/tcp
```

Wait for a minute or two...

... and Sonarqube is ready!

Eg: [https://sonar.aws.melvincv.com/](https://sonar.aws.melvincv.com/)

Login with the default credentials and change your password ASAP.

user: admin

pass: admin

### **Sonarqube Integration with Jenkins**

A sonarqube token is necessary for Jenkins to authenticate to your sonarqube instance.

In Sonarqube, go to Administrator &gt; My Account &gt; Security &gt; Generate Token

![](https://lh7-us.googleusercontent.com/InOgREyJSeoZ42YDAksr2dbT1Ug0n9-YVkFn73qbfOKsAJe4diAKB-uGu6GGz1W7WNIHUWE_MqH-HXvUIGw7zoSeUm73Atl8p4ynRH23BKh-DDZe_r1NeuP9P7PPuZXJ7xSDTIgytNQ2bMlGUAwGdcI align="left")

Copy the token and add it as a Jenkins credential of type "Secret Text" with id "sonar-token"

![](https://lh7-us.googleusercontent.com/gxLmGe2h5SlJ9fqJXi1Rp7RWa7nhGLNG98TZIdIf0I5SHxp2AMOYQ8lPoLy0i4mBLkoQLYx2HDxcXm8pRbVMyjP1BxXMqnG1q_uXIYJnW-sdQy9d82eblC2T0leo25TVfi5ZzEj6tCiNt3r_4raNd64 align="left")

Now add a Sonarqube Server.

Manage Jenkins &gt; System &gt; SonarQube servers &gt; SonarQube installations

* name: sonar
    
* server url: [https://sonar.aws.melvincv.com/](https://sonar.aws.melvincv.com/)
    
* token: sonar-token (select yours)
    

![](https://lh7-us.googleusercontent.com/nn-0LtTltSFUmc9nxEc8hIvwklcGMJfFwP-47OsNjK2spIsYLr0fqvwqSm4Hioj3VKqLydJxXSox5BZpcM7GIrrTfK2vlKimRh6U-RsFIb2eOn44kQIqeyp8F9taQfk_zDjEjgFo5MO1Krog4DR_LMY align="left")

The connection to Sonarqube is set up. After code scanning and quality gate analysis, Sonarqube needs to contact Jenkins with the stats. For this, you need a Webhook. Add a Webhook pointing to a URL on your Jenkins instance.

![](https://lh7-us.googleusercontent.com/QSTr2nFS6rRMBO4pKnGj7Sh_CRASVaBYc08rbssSKy2GwWAHx3yB1rkMAmGi2NQnFDb2hTRHoQbFgnWKyXYShuPuc3MwQQ0BY2djfzZ1r2tTpSiY6lmPTFWvAG0jpQyEEWpZA38skLVPKMNyvDu-vsk align="left")

URL would be the IP / Domain of your Jenkins with `/sonarqube-webhook/`

Eg: [https://jenkins.aws.melvincv.com/sonarqube-webhook/](https://jenkins.aws.melvincv.com/sonarqube-webhook/)

## **Build the Jenkins Job**

Go to the Jenkins Pipeline and "Build with Parameters". Leave them as is and click Build. The Code Quality and Test stage should pass.

![](https://lh7-us.googleusercontent.com/T2fmQ_Q46B_mBjAsgrEPkCPjJbDrflRO_lBlQgTj0Jx6CHGwlWIz2pNn9gbS8MvXTSBghBVnwGDc1rT-MJ6ww0DWVGD1MMogqYykOhZD7CuoX1QXLZsPtNmQNhgOWbsLRchFNyNEyXRFckkTPJfan_E align="left")

The "CD to Server" stages will fail as the servers are not ready yet.

## **Start and Configure the CD Servers using Ansible**

Start 2 EC2 instances with the following parameters:

* name: cd-target-svrA, cd-target-svrB
    
* ami: Ubuntu 22.04 LTS 64 bit
    
* type: t3.micro
    
* security group: open ports 8080, 22
    
* spot instance: true
    

![](https://lh7-us.googleusercontent.com/DwLWspItX-Vsg7aVH1nWbwZv3SaV2lN7SyyIkEW0NvWlctet26N-OlsLebIQsaQQ8R4_IZ16QtQJaJneCMo3Yvwz1tG46CKeT8ycpCNs7ijDdbj6Xg01vhLh3rsc5AuKQBoVec61krICZ3kBCqoiNbQ align="left")

Create an inventory file and an Ansible playbook to install Tomcat on both servers.

Refer my Ansible files: [https://github.com/melvincv/java-basic-practice-app/tree/main/ansible](https://github.com/melvincv/java-basic-practice-app/tree/main/ansible)

Set up an Ansible Controller locally and run ansible ad-hoc command to ping the servers

```bash
ansible all -i inventory.ini -m ping
```

![](https://lh7-us.googleusercontent.com/sCHDPHr6LsHznEOxxZpo8MZs3TjJ24IOA9KcY8ex6zAR7irhhF3Hsckuru7E_ZM9Bk-fH111IDj-0OkLEYhQCe34cet_9TtOGpynThtFGfhheQZqbMbDRqjlWmntuia5pXPsOl_DRaANyVZt5DNzbZ4 align="left")

Run the Ansible playbook after you get a successful ping.

```bash
ansible-playbook -i inventory.ini playbook.yml
```

![](https://lh7-us.googleusercontent.com/yMnwVmZ3NNIlsCLJJ1JaLjS3dB7pfNkzhl4tm2S5_hi-2MW2Hn589lYPOyIzVQDPIhH1ABDr4_rf9tRjv_4kFweg3bAXDjJWO2_RXM3PfS0ZMeOZofMAujbu4T6w4d6OBNZnD5N1XPwqUelJDigGqns align="left")

Now you have tomcat running in both instances.

You will be able to access the default page of Tomcat using the IP and Port

Eg: [http://43.206.158.1:8080/](http://43.206.158.1:8080/)

## **Continuous Deployment using Jenkins**

We had installed the SSH Agent plugin earlier.

Now create a Jenkins Credential of type "SSH Username with private key". Jenkins will use this to log in to your target servers.

Set an ID and username and enter the Private Key.

![](https://lh7-us.googleusercontent.com/GyjP9yELsvmxJHk0Va4VQ7V9ujLGduSWW2qeoAbSDwOyDYRTxx7sQxJfMQNTeSLGea9s7VYFV6wj9sPYeb_i-65GBfM2cAGi4a72BWdiPJnQmRurzbCOJTJwasaAKYUh8NvX3PBzueYefQInJwpU2Z8 align="left")

![](https://lh7-us.googleusercontent.com/20ZOgjOAQciiF9hR2nDDuuOTd5jospbqvs-byTM3ZXZeQBckJri8xs4sDca8R_4EKqCcbFxywj_22m0r-BzxSO4A0k7dOkrCgrTzDTpPeX9_BlrlcSJXItc2gGWhi0huXPU7ypWwr4uR10_2BNG2P10 align="left")

Modify the Jenkins Pipeline to reflect the new ID

Stages: **CD to Server A** and **CD to Server B**

```bash
sshagent(credentials: ['akhil-p1-ssh-key'])
```

Run the Build with Parameters. Jenkins should now be able to deploy the WAR file to both instances.

![](https://lh7-us.googleusercontent.com/1G-K1NqVYjmzIFCkD-RGwsFhURb6ryZK9QKDiFgf1LGYavLszJYlTxzbewAfL5HcmyUBR0jYG8Fs5IUVLvi2z7uyLGgT2u4ZgOaW9OtTueQcDdDrIA9jbOQ9Imj3hKibBgaVq5uRisPpOx0B7YftnpY align="left")

## **AWS Setup**

### **Create a Target Group**

EC2 &gt; Target Groups &gt; Create Target Group

Basic Config &gt; Instances

Target group name = akhil-p1

Protocol : Port = HTTP 8080

VPC: default

Protocol version: HTTP1

Health check path: /

### Register targets

Add the Server A and B to the target group.

![](https://lh7-us.googleusercontent.com/QCg-02R-q3B8CX0nQt0k86l_pgV8vIVpnnp9zjPx0T3J_x9PvCbgCszO1zR50XhSCsshF1NOwbwHRUNmHAyqdVD2OSDX_OkRyRuEOduMnQ3OmTLL733kkxJviAePxClWjt1yvgjMDA9RIGtbUVcLbGQ align="left")

![](https://lh7-us.googleusercontent.com/OZWpSFExouCsjkvy7KTX3Q-BQpzsmOPJ1uydmv90bNnPye0_qAiI5BYJCVIdUrV8Sykpsq1zx1hwXVLN0G_at85lIfHXbnANXclf7pf_3ki3WE5CotulwHZRTdNCTa7S_fXRoPPTfKDQrbcAV3zCFaA align="left")

### **Application Load Balancer**

EC2 &gt; Load Balancers &gt; Create &gt; Application Load Balancer

Internet facing, IPv4, default VPC, select all subnets, 

Create a Security Group

* open ports 80, 443
    

Listeners and routing 

* select your target group
    

Create ALB  
Get it's DNS Name: [ahkil-project-1-1938854889.ap-northeast-1.elb.amazonaws.com](http://ahkil-project-1-1938854889.ap-northeast-1.elb.amazonaws.com)

## Blue / Green Deployment

The pipeline has many parameters that we can select at build time.

![](https://lh7-us.googleusercontent.com/9wmm_HqgzT7pSKT9TtqR4-GT2lQ_Qgp7tpT9DdVLEZqrCuBY-m4MtPkrRMMdKQIZ0u9S1en1ocm3oToGJq6On4OtS3Cv3vQ2o7kaWk8EWeD8G6RWNhq7nu3Ztbo_oOiOXQ9iWGxMO0QorwpUplBzlfU align="left")

While deploying a new version of the app, you can select which server to deploy to using the check boxes for SERVER\_A and SERVER\_B. These are Boolean Parameters in the pipeline and are "true" by default. This will deploy the app to both servers. 

To deploy it only to server B, uncheck server A. 

Made a change to the "message" in src/main/java/com/akhilsharma/web/[HelloController.java](http://HelloController.java)

Commit and Push

Build with Parameters &gt; Select Server B Only

![](https://lh7-us.googleusercontent.com/9mFsuwKd_hMfRpC_jR9UQEbdd6t64X2DPhiUxTWzuhAhnCFjVKM_duI0nVv52CRjMAeLRLi6mjD8WBcwWedqsERUk26HJ_3QpNJQIeg6Rwn4R1BrH7zqjkph5529LxXeQ0kx8Zno8qETzIdWsiSda7k align="left")

Now access the ALB DNS name. You can see 2 versions of the app upon refreshing the page.

## **SSL Setup**

Route 53 &gt; Select Zone &gt; Create Alias record

![](https://lh7-us.googleusercontent.com/zY6CijoRBM8Eu8JAAlJdDS-RyK5sTOQz1HumeUEWjNwWJTXEz0WOvTPgSeF-ItCLNtK4vTguwc6W_2ilfH0suf6y__c72kVRB8u3GWID8CT6Vbs20CsWaFIoubfj3l2ftrvITYENtHckOFQKq3wVua0 align="left")

[http://javaapp1.aws.melvincv.com/](http://javaapp1.aws.melvincv.com/) 

Your URL is now available.

### **Create an SSL certificate using AWS ACM**

Add a CAA Record for your zone

**Amazon will not issue a certificate without this record.**

Add the below value to the record. You may use `issuewild` or `issue`. `issuewild` is used to authorize the issue of wildcard SSL certificates.

Value:

```bash
0 issuewild "amazon.com"
0 issuewild "amazontrust.com"
```

![](https://lh7-us.googleusercontent.com/rMXUAGMtVmFFtJvGRX9WeUFg9ZjKeFz15Dpztaquhz4rT4ODCnBNsFwQmk4fJ3PU1jMkRz27UlItMvDdJg8Ig7BdgzsRrDnPwx5rWffT1fuNoJyX4kAKoXm2mP4C5Bq0Qk4gG3UQuyPhJ8hr_k3Df0g align="left")

![](https://lh7-us.googleusercontent.com/xQJMQgQ_yKe7-O2RLsTyhmIQpTxSwxH9ppfhlZ8bT5yTP9QRuj37Us13w_N3C7-yKseav4a3Q3nu8tQul7S_kk2MA7RfnFzViRwh72EoFy-9zf765ka7oUEah4Y2fR5K_e7OrSE2XnCMqvg8Rx0A2kQ align="left")

AWS Certificate Manager &gt; Request a public certificate &gt; 

FQDN:  \*.aws.melvincv.com

Key algorithm: RSA

Create CNAME record in Route 53 (or automatically)

![](https://lh7-us.googleusercontent.com/HOoOunWJILQaxq3W2XXAZ9Utu5BOgXk_UBweaE72dWdRiivQcjWp0O2pRYanfSsQM3_p2WHob_yVwdt9vsfPfo0gFN2jvgghTTjFKvjRVs8wszfpAkLTWDeu6mp851QRBJ_WqbGOSxkVZTmAyR78F3U align="left")

### **Configure App Load Balancer**

EC2 &gt; Load balancers &gt; ahkil-project-1

**Listeners and rules**

Add listener &gt; HTTPS 443

Forward to target group &gt; akhil-p1

Default SSL/TLS server certificate &gt; \*.aws.melvincv.com

**Forward HTTP to HTTPS**

HTTP listener &gt; Edit Rules

![](https://lh7-us.googleusercontent.com/gPaWXhdknj6Of-LMjmG6Bbgxl1AtTbxzs7GkTw-JOsuL8L7UZCcVz3PSUu0_fxlimmHGZIS43RWO1P3JSgFxHn7951HDtxlKCqlWCpF1fGDqEIpjk4RshrEMi1yu98uuEC2n6mxBEvPyACTPhwWddmI align="left")

Edit Rule &gt; Default &gt; Redirect to URL

![](https://lh7-us.googleusercontent.com/PXI5U_QMTs7diCk8qQG_v6nr1MmDqPSecDN0ajIU2tftChaFs7_r6cmcsUiUQ-XAeT81GQYwmO0eEs65htoB6zR7Pw1DQmuIWAnr8AylmmmYoG-bGqkX5CI6JYT6wfORqFiGJkCinQ5Nid3-2kRwxtM align="left")

![](https://lh7-us.googleusercontent.com/zlSf4GWqHW6Tijsr8T7yK9FNr-bNI_Qs9TyvXlIQC0hGMLgYJChVTdCH_H4znsW9aqgDnyqWnP5Jr42Cj3dXQKaIcEpDfWnwijLx5mZhEJxEPEKAbk-Z-zUa6pqgnWi8D3-h36tkEpvh5YtNZSsx2oQ align="left")

![](https://lh7-us.googleusercontent.com/qKTiPe2BjC6FmTcIejGw_Ed01TXCx0PeA4mULdCzbsGDg2es7bE23Izz-dflhtCrMlFgRCVrSAwkLBCuKBVdt_xIH80_buaVlGWEML8mFrjOJMrS88OSOdcyi7lCZ2ubc8Euuyo7GHHAhtaELH6997E align="left")

# **Results**

On accessing the URL and refreshing the page, we get 2 versions of the app with different messages due to the blue/green deployment.

![](https://lh7-us.googleusercontent.com/EQbwKlF1MqCw0DNbHX3pTEGtKjZaebOSuw_iyeHJ8_g7dHLb1SCHkdRn_uTVKxPizTVGMrHs_P8zPBBCab28pBnTAqtumM4uo-aOYrZL3feGrnlT5XplJgN8LR3iZ-5ip_Kp9idlD4T2CaeeCrLH4L4 align="left")

![](https://lh7-us.googleusercontent.com/d1-1vInwFPjbBt2aPdfGt_qIYzEm7nItV8u6rD2U9bhRF3rHgH-YccxEikJa3MFE433noV0zahsE6zlU92mHOk7VpakwRdkf0_eXto0IRbpjy3Var0fE4huoKp092vHoKkjuC5fsupn-HGzj8lj7DXM align="left")

## Jenkins Console Output

[Blue/Green Deployment](https://docs.google.com/document/d/1we90623_GcuqjKwXQz3QNYi1EIUwJOcj17U3SDECrxc/edit?usp=sharing)

[Normal Deployment](https://docs.google.com/document/d/1BigCrXl7nWI4lklMCUXHk54CzS0Qps1dtnmuDHe2XUE/edit?usp=sharing)

---

Hope you found this article useful. You are free to make modifications to this project. Linking to the original article will be much appreciated! Let me know what you think of this project in the comments!
