As you continue to develop your software, you must also continue to integrate it with previous code and deploy it to servers. Manually doing this is a time-consuming process that can occasionally result in errors. So we need to do this in a continuous and automated manner, which is what you will learn in this article.
Let's Get Started
The Problem
Consider our productivity app. There are numerous steps we must complete, from building the application to pushing it to the docker hub. First, we must run tests with a command to determine whether all tests are passed or not; if all tests are passed, we build docker images and then push those images to Docker Hub. If your application is extremely complex, you may need to take additional steps. Now, we're doing everything manually, which takes time and can lead to mistakes.
The Solution
To address this problem, we can create a CI/CD Pipeline. So, whenever you add a feature or fix a bug, this pipeline is triggered, which automatically performs all of the steps from testing to deploying.
What is CI/CD and Why it's Important?
Continuous Integration and Continuous Deployment is a series of steps performed to automate software integration and deployment. CI/CD is the heart of DevOps.
From development to deployment, our MERN app goes through four major stages: testing, building docker images, pushing to a registry, and deploying to a cloud provider. All of this is done manually by running various commands. This must be done every time a new feature is added or a bug is fixed. However, this will significantly reduce developer productivity, which is why CI/CD is required to automate this process. (In this article, we will cover till pushing to the registry.)
What is "GitHub Actions"?
A CI/CD server is required to run a CI/CD pipeline; this is where all of the steps written in a pipeline run. There are numerous services available on the market, including Jenkins, Travis CI, Circle CI, GitLab CI/CD, AWS CodePipeline, Azure DevOps, and Google Cloud Build. I chose GitHub Actions because it is natively integrated with Git, works well with GitHub, and is extremely simple to use. It is also sufficient for small/medium-sized applications such as ours. For more complex applications, you can use any of the other services listed above.
How do GitHub Actions work?
GitHub actions is not a CI/CD platform, CI/CD is one of its use cases.
Events
Workflows are triggered by events such as pull requests, pushed, PR merged, the issue created, and a few others. For example, you can trigger a workflow running a bot that says "Thanks for opening the issue, our maintainers will review it." whenever an issue is created.
Workflows
Workflows are YAML files that contain all of the instructions that are executed after an event is triggered. Workflow files are typically kept in the .github/workflows/
directory.
Jobs
Jobs is a group set of steps called a job. Each workflow must include at least one job. A job is a set of steps that must be executed in a virtual environment.
Runners
This is my favorite feature of GitHub Actions. Runners are virtual environments where jobs are carried out. You can either use GitHub-hosted runners that contains pre-installed packages or host your own.
Adding a CI/CD Pipeline to the MERN App
Prerequisites
Get source code from here if haven't followed previous tutorials in this series.
Read previous articles in this series so that you won't get confused.
Basic YAML understanding.
GitHub account.
To begin, we must define pipeline configurations in a YAML file. Where should I save this YAML file? - If you're familiar with GitHub, you've probably heard of the .github
directory, which is a special directory used for GitHub-specific items such as issue templates, pull request templates, workflow files, and so on. So we must place our pipeline file in the workflows
folder, which is located in the .github
folder.
Go ahead and create a YAML file and name it something like pipeline.yml
or pipeline.yaml
. Your folder structure should now look like this:
.
└── my-project/
├── .github/
│ └── workflows/
│ └── pipeline.yml
├── client/
├── server/
└── docker-compose.yml
First, we have to mention the name of the workflow.
name: Build and Deploy
Then, mention a trigger - on commit/on push/on pull request, etc.., along with branches.
on:
push:
branches:
- main
Since our application uses some environment variables while running tests, we have to set environment variables in the YAML file that will be used by workflow steps.
But, first, let's add environment variables in the GitHub repository. To do so, go to repository Settings -> expand Secrets and variables -> Actions.
Click on New repository secret and add the following secrets.
Now in the YAML file, you can access them like this:
env:
MONGODB_URI: ${{ secrets.MONGODB_URI }}
TOKEN_KEY: ${{ secrets.TOKEN_KEY }}
EMAIL: ${{ secrets.EMAIL }}
PASSWORD: ${{ secrets.PASSWORD }}
Let's get to the interesting part! These are the only steps we have to perform with GitHub Actions. As simple as that.
Start by adding jobs, let's add a job called build-and-deploy
.
jobs:
build-and-deploy:
# This is telling GitHub to run the workflow on the latest version of Ubuntu.
runs-on: ubuntu-latest
Adding Continuous Integration
In this job, let's add steps to pull code from the repo and run some tests.
steps:
# Checkout the code from the GitHub repository
- name: Checkout code
uses: actions/checkout@v3
# Install dependencies and run tests for the client application
- name: Install and Test Client
working-directory: ./client
run: |
npm install
npm run test
# Install dependencies, export environment variables to be used by application and run tests for the server application
- name: Install and Test Server
working-directory: ./server
run: |
npm install
export MONGODB_URI=$MONGODB_URI
export TOKEN_KEY=$TOKEN_KEY
export EMAIL=$EMAIL
export PASSWORD=$PASSWORD
npm run test
name
- You can provide a name for each step.uses
- Set which action to use.checkout
is a pre-defined action provided by GitHub that checks out a repoworking-directory
- Set working directory. This is where all the commands mentioned in that step will run.run
- Run commands. You can run multiple commands by putting|
(pipe). Learn more about YAML in this YouTube tutorial.
Now if you push these changes to GitHub. It will automatically run your workflow. You can see the running workflow in the Actions tab of your GitHub repository.
Click on the commit and then the job to see the running steps.
Adding Continuous Deployment
Now it's time to build docker images and push them to the docker hub.
# Build a Docker image for the client application
- name: Build Client Docker Image
working-directory: ./client
# Build image with tag rakeshpotnuru/productivity-app:client
run: |
docker build -t rakeshpotnuru/productivity-app:client-${{github.run_number}} .
# Build a Docker image for the server application
- name: Build Server Docker Image
working-directory:
./server
# Build image with tag rakeshpotnuru/productivity-app:server
run: |
docker build -t rakeshpotnuru/productivity-app:server-${{github.run_number}} .
# Login to Docker Hub using credentials from repository secrets
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# Push the Docker images to Docker Hub
- name: Push Docker Images to Docker Hub
run: |
docker push rakeshpotnuru/productivity-app:client-${{github.run_number}}
docker push rakeshpotnuru/productivity-app:server-${{github.run_number}}
Before pushing to the docker hub we need to log into the hub. That's why there's a step before pushing images. With
with
we can use secrets directly without setting them in the environment.github.run
_number
- This is because it's a good practice to give a unique identifier for every image on the hub. And this environment variable (run_number
) will be generated automatically by GitHub Actions.
Here's the complete YAML file.
# The name of the workflow.
name: Build and Deploy
# Run the workflow when code is pushed to the main branch
on:
push:
branches:
- main
# Set environment variables
env:
MONGODB_URI: ${{ secrets.MONGODB_URI }}
TOKEN_KEY: ${{ secrets.TOKEN_KEY }}
EMAIL: ${{ secrets.EMAIL }}
PASSWORD: ${{ secrets.PASSWORD }}
# This is the workflow that is being run.
jobs:
build-and-deploy:
# This is telling GitHub to run the workflow on the latest version of Ubuntu.
runs-on: ubuntu-latest
steps:
# Checkout the code from the GitHub repository
- name: Checkout code
uses: actions/checkout@v2
# Install dependencies and run tests for the client application
- name: Install and Test Client
working-directory: ./client
run: |
npm install
npm run test
# Install dependencies, export environment variables to be used by application and run tests for the server application
- name: Install and Test Server
working-directory: ./server
run: |
npm install
export MONGODB_URI=$MONGODB_URI
export TOKEN_KEY=$TOKEN_KEY
export EMAIL=$EMAIL
export PASSWORD=$PASSWORD
npm run test
# Build a Docker image for the client application
- name: Build Client Docker Image
working-directory: ./client
# Build image with tag rakeshpotnuru/productivity-app:client
run: |
docker build -t rakeshpotnuru/productivity-app:client-${{github.run_number}} .
# Build a Docker image for the server application
- name: Build Server Docker Image
working-directory:
./server
# Build image with tag rakeshpotnuru/productivity-app:server
run: |
docker build -t rakeshpotnuru/productivity-app:server-${{github.run_number}} .
# Login to Docker Hub using credentials from repository secrets
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# Push the Docker images to Docker Hub
- name: Push Docker Images to Docker Hub
run: |
docker push rakeshpotnuru/productivity-app:client-${{github.run_number}}
docker push rakeshpotnuru/productivity-app:server-${{github.run_number}}
Resources
Learn more about CI/CD and GitHub Actions.
GitHub Actions 101 written series by Victoria Lo.
Introduction to GitHub Actions CI/CD by Kunal Kushwaha.
CI/CD Tutorials YouTube playlist by TechWorld with Nana.
In the next article, we will learn how to deploy to a cloud service provider (Generally the last step in a CI/CD pipeline).
Build strong pipelines. Don't be like this 💀 - https://youtu.be/M_jjG9K5KEo
Follow me for more, seriously!