CI/CD Tutorial for Beginners

CI/CD Tutorial for Beginners

CI/CD Tutorial for Beginners

The purpose of this blog is to serve as a step-by-step guide for beginners looking to implement their first CI/CD pipeline using Docker and Jenkins. By following this tutorial, readers will create a simple "Hello World!" web application and automate its build, test, and deployment processes. This hands-on approach introduces the core concepts of Continuous Integration and Continuous Deployment, equipping readers with practical skills for modern software development practices.

Table of Contents

Introduction

CI/CD, which stands for Continuous Integration and Continuous Deployment (or Delivery), is a set of practices that automates the process of software development, testing, and deployment. Continuous Integration (CI) involves developers frequently merging their code changes into a shared repository, often multiple times a day. Automated tests are then run to ensure that new code integrates smoothly with the existing codebase, identifying any issues early in the development cycle.

Continuous Deployment (CD) goes a step further by automatically deploying every change that passes the CI process to production. In environments where Continuous Delivery is practiced, the deployment step is automated, but it requires manual approval to go live.

CI/CD is essential because it reduces the time it takes to deliver software, ensures higher quality through automated testing, and minimizes the risk of bugs and errors in production. By continuously integrating and deploying, teams can respond faster to changes in the market or customer requirements, allowing for more agile development processes.

Practical applications of CI/CD are widespread in today's technology landscape. For example, companies like Amazon and Netflix deploy updates to their production environments hundreds or even thousands of times per day, ensuring they can deliver new features and fixes to users quickly. In web development, CI/CD pipelines are used to automatically test and deploy web applications, ensuring that they remain up-to-date and functional across all environments. This approach is also critical in DevOps practices, where collaboration between development and operations teams is streamlined to enhance the overall software lifecycle.

Step 1: Setting Up the Environment

1.1 Prerequisites

Before starting, ensure you have the following installed:

  • Git: For version control.
  • VSCode: As your code editor.
  • Jenkins: As the CI/CD tool.
  • JAVA 11 (Adoptium): As an Engine to run Jenkins.
    Windows Note: Change the GPO (Group Policy Object) [secpol.msc] > Local Policies > User Rights Assignment > Find and double-click on "Log on as a service" > Add a domain user (your user) to the list.
    Windows Note 2: Select the root jdk-11.0.24.8-hotspot of the JDK in Jenkins, not the /bin
  • Docker: For containerizing the application.
  • Heroku CLI: For deploying applications to Heroku.
  • Node.js: For creating a simple web application (we'll use an Express.js app).

1.2 Create a Public GitHub Repository

  1. Create a directory on your local computer to hold the cloned repository.
  2. Go to GitHub and create a new public repository.
  3. In the terminal on your local computer, open the local directory, and clone the repository using the following command:
    git clone https://github.com/your-username/your-repo-name.git
    
  4. Open the cloned repository in VSCode.

Step 2: Create a Simple Application

2.1 Initialize a Node.js Project

  1. In the root of your project, in the terminal, initialize a new Node.js project:
    npm init -y
    
  2. Install Express.js:
    npm install express
    
  3. In the root of your project, create a file named app.js, and add the following code:
    const express = require('express');
    const app = express();
    const port = process.env.PORT || 3000;
    
    app.get('/', (req, res) => {
        res.send('Hello World!');
    });
    
    app.listen(port, () => {
        console.log(App listening at http://localhost:${port});
    });
    

2.2 Create a Simple Test

  1. Using the terminal, install mocha and chai for testing:
    npm install --save-dev mocha chai
    
  2. Create a /test directory with a file test.js with the following content:
    const chai = require('chai');
    const chaiHttp = require('chai-http');
    const server = require('../app');
    
    chai.use(chaiHttp);
    const should = chai.should();
    
    describe('Main Route', () => {
        it('should return Hello World on / GET', (done) => {
            chai.request(server)
                .get('/')
                .end((err, res) => {
                    res.should.have.status(200);
                    res.text.should.equal('Hello World!');
                    done();
                });
        });
    });
    
  3. Modify app.js to export the app (for testing purposes):
    const express = require('express');
    const app = express();
    const port = process.env.PORT || 3000;
    
    app.get('/', (req, res) => {
        res.send('Hello World!');
    });
    
    app.listen(port, () => {
        console.log(App listening at http://localhost:${port});
    });
    
    // Export the app for testing purposes
    module.exports = app;
    
    
  4. Modify package.json to add a test script:
    {
      "name": "your-app-name",
      "version": "1.0.0",
      "description": "A simple Node.js app",
      "main": "app.js",
      "scripts": {
        "start": "node app.js",
        "test": "mocha"
      },
      "author": "Your Name",
      "license": "ISC",
      "dependencies": {
        "express": "^4.17.1"
      },
      "devDependencies": {
        "mocha": "^9.0.0",
        "chai": "^4.0.0"
      }
    }
    

Step 3: Containerize the Application with Docker

3.1 Start the Docker Engine

  1. Launch the Docker application on your local computer.

3.2 Create a Dockerfile

  1. In the root of your project, create a Dockerfile with the following content:
    # Use an official Node.js runtime as a parent image
    FROM node:14
    
    # Set the working directory in the container
    WORKDIR /usr/src/app
    
    # Copy package.json and package-lock.json
    COPY package*.json ./
    
    # Install dependencies
    RUN npm install
    
    # Copy the rest of the application code
    COPY . .
    
    # Expose the port the app runs on
    EXPOSE 3000
    
    # Command to run the application
    CMD ["node", "app.js"]
    

3.3 Create a Docker Compose File (Optional)

If your application consists of multiple services (e.g., a database), you can create a docker-compose.yml file. For now, we'll keep it simple and just use the Dockerfile. See here for more details.

3.4 Build and Run the Docker Image Locally

  1. Build the Docker image:
    docker build -t your-app-name .
    
  2. Run the Docker container:
    docker run -p 3000:3000 your-app-name
    

You should be able to access your app at http://localhost:3000.

Step 4: Set Up CI/CD with Jenkins

4.1 Set Up Jenkins

  1. Install Jenkins on your local machine or use a cloud-based solution like Jenkins on AWS.
  2. Install necessary plugins:

4.2 Create a Jenkins Pipeline

  1. In Jenkins, create a new pipeline project.
  2. Set up GitHub integration by connecting your GitHub repository to Jenkins.
  3. Create a Jenkinsfile in the root of your project with the following content:
    pipeline {
                agent any
    
                environment {
                    DOCKER_IMAGE = "your-dockerhub-username/your-app-name"
                    HEROKU_APP_NAME = 'your-heroku-app-name'
                    HEROKU_API_KEY = credentials('HEROKU_API_KEY')
                }
    
                stages {
                    stage('Checkout') {
                        steps {
                            git branch: 'main', url: 'https://github.com/your-username/your-repo-name.git'
                        }
                    }
    
                    stage('Build Docker Image') {
                        steps {
                            script {
                                dockerImage = docker.build("$DOCKER_IMAGE:$BUILD_NUMBER")
                            }
                        }
                    }
    
                    stage('Run Tests') {
                        steps {
                            script {
                                dockerImage.inside {
                                    sh 'npm install'
                                    sh 'npm test'
                                }
                            }
                        }
                    }
    
                    stage('Push Docker Image') {
                        steps {
                            script {
                                docker.withRegistry('https://registry.hub.docker.com', 'dockerhub-credentials') {
                                    dockerImage.push("$BUILD_NUMBER")
                                    dockerImage.push("latest")
                                }
                            }
                        }
                    }
    
                    stage('Deploy to Heroku') {
                        steps {
                            sh 'heroku container:login'
                            sh 'heroku container:push web --app $HEROKU_APP_NAME'
                            sh 'heroku container:release web --app $HEROKU_APP_NAME'
                        }
                    }
                }
    
                post {
                    success {
                        echo 'Deployment Successful!'
                    }
                    failure {
                        echo 'Deployment Failed!'
                    }
                }
            }
    
  4. Jenkins Configuration:
    • Ensure you have Docker installed on your Jenkins server.
    • Add your Docker Hub credentials to Jenkins:
      1. Go to Jenkins > Manage Jenkins > Manage Credentials
      2. Add Docker Hub credentials with the ID dockerhub-credentials.
    • Add your Heroku API Key as a secret text credential with the ID HEROKU_API_KEY.

Step 5: Deploy the Application to Heroku

5.1 Set Up Heroku

  1. Create a new Heroku app using the Heroku CLI:
    heroku create your-heroku-app-name
    
  2. Deploy manually using Docker to ensure everything works:
    heroku container:push web --app your-heroku-app-name
    heroku container:release web --app your-heroku-app-name
    

Step 6: Automate the CI/CD Pipeline

  1. Push some changes to your GitHub repository.
  2. Jenkins should automatically start the pipeline, building the Docker image, running tests, pushing the image to Docker Hub, and deploying it to Heroku.
  3. Verify that your app is running on Heroku by visiting https://your-heroku-app-name.herokuapp.com.

Step 7: Expand and Explore

Now that you have a Docker-based CI/CD pipeline set up, consider expanding the tutorial by:

  • Adding Linting: Integrate ESLint into your pipeline.
  • Code Coverage: Integrate code coverage tools like nyc and display the results in Jenkins.
  • Notification: Set up Slack notifications for pipeline success/failure.
  • Environment Variables: Manage different environments (e.g., staging, production).

Conclusion

This interactive learning program introduces you to the basics of CI/CD using Docker alongside other industry-standard tools. By following this tutorial, you should gain a solid understanding of how to set up and maintain a Docker-based CI/CD pipeline, enabling you to develop and deploy applications more efficiently in a containerized environment.

.dockerignore

node_modules
npm-debug.log
.env
.vscode/
.idea/
.git
.gitignore
Dockerfile
docker-compose.yml
test/

.gitignore

# Node.js dependencies
node_modules/
npm-debug.log
yarn-error.log
package-lock.json
yarn.lock

# Logs
logs/
*.log
logs/*.log

# Runtime data
pids/
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov/

# Coverage directory used by tools like istanbul
coverage/
*.lcov

# Dependency directories
node_modules/
jspm_packages/

# Optional npm cache directory
.npm

# Optional REPL history
.node_repl_history

# IDE specific files
.vscode/
.idea/
*.sublime-project
*.sublime-workspace

# Environment variable files
.env
.env.test
.env.production
.env.local

# Docker
Dockerfile
docker-compose.yml
.dockerignore

# Build artifacts
dist/
build/
tmp/
temp/

# Generated files
*.swp
*.swo

# Operating system specific files
.DS_Store
Thumbs.db

# Git specific files
.git/
.gitignore

# Jenkins-specific files
jenkins/

View me!