What are GitHub Actions and How can we automate application deployments using GitHub Actions?
Nowadays, developing software without a CI/CD pipelines and without a DevOps process is unimaginable. Your version control is tied up to the operational process so that the piece of the software is deployed on the production servers. Understanding DevOps really involves understanding these common problems and recognising solutions to them.
The common theme percolates through these solutions is the bringing together of the traditionally separate worlds of Development and Operations.
What is covered in our article?
We will learn what are GitHub actions, deploy our Node.js application using Github actions and creating a complete CI/CD pipeline workflow and
many more....
Pre-requisites
- Install Nodejs and npm on your workstation
- Create an AWS Cloud account
What are Github Actions?
The concept of using tools for CI/CD isn't new. Many companies and teams leverage Jenkins, CircleCI and Travis CI to accomplish deployment workflow and automate things by running scripts in CI job.
I have already used a bit of Jenkins as a continuous integration server and wanted to try Github Actions. The idea here is to automate the entire CI/CD pipeline using Github Actions if you are storing your code on Github and deploy our application on the remote server.
Github Actions allow creating a complex workflow using automated tasks (called actions) in order to be activated and run when a commit is done in your Github repository. You can choose which kind of operating system will be used to run actions: Ubuntu Linux, Windows or Mac OS X.
Advantages of using Github Actions Workflow
- The individual actions in a workflow are isolated by default. You can use a completely different computing environment for, say, compilation and testing.
- There are already open-source reimplementations of GitHub actions, such as an action for local testing.
- Ready actions in the GitHub marketplace.
- No need to set up webhooks and access tokens for connecting your CI/CD service to GitHub โ because GitHub Actions is already in GitHub!
- Configs will always be stored in .github
So, let us go ahead and execute below commands, write some code and automate deployments by leveraging Github Actions.
- Create the project folder
mkdir nodejs-github-actions
npm init
- Create a package.json file into the root of our project folder nodejs-github-actions
If you want to skip all the question asked during the creation of package.json with the above command, run:
npm init -y
- Install the latest version of packages needed to develop our application
npm install express --save
Express Js
Express.js is a minimal and flexible Nodejs framework which provides lots of features to develop web and mobile applications. It's easy to create an API with HTTP utility and middlewares with Express.js
- Create a server.js file into the src of our project folder nodejs-github-actions
mkdir src
touch src/server.js
- Insert the below code
const express = require('express')
const app = express();
const port = process.env.PORT || 3000
app.listen(port,() =>{
console.log('server is up on ' + port);
})
- Create the routes folder inside the src folder. We would be creating all our routes here.
mkdir src/routes
touch src/routes/index.js
- Create some routes for our demo project. Insert the below code inside routes/index.js
const express = require("express");
const router = new express.Router();
router.get("/", (req, res) => {
res.status(200).json({ message: "hello" });
});
router.get("/dashboard", (req, res) => {
res.status(200).json({ message: "this is a dashboard" });
});
router.get("/user/profile", (req, res) => {
res.status(200).json({
name: "john doe",
age: "30",
location: "Mumbai",
});
});
module.exports = router;
- Now update the server.js file to include routes
const express = require("express");
const indexRoutes = require("./routes/index");
const app = express();
const port = process.env.PORT || 3000;
app.use(express.json());
app.use("/api", indexRoutes);
app.listen(port, () => {
console.log("server is up on " + port);
});
- Check if our application is working fine
npm run start
Push our code to GitHub
- Create .gitignore file to exclude node_modules from getting pushed to our git repo
touch .gitignore
vim .gitignore
#add below line
node_modules
- Now, let's push our code to GitHub.
#initialize git
git init
#Add all the files
git add .
#commit
git commit -m "nodes GitHub actions"
#set the remote repository if you have not set it up
git remote add origin https://github.com/desaijay315/nodejs-github-actions.git
#push the code
git push origin master
- Let's go to Github Actions
- Create the Nodejs Workflow.
- Click on start commit and commit the file to the master branch. This will create the workflow file for nodejs application.
- Take a git pull on the project folder. This will fetch the /.github/workflows/node.js.yml file which was just created in the workflow step
git pull
- Insert the below code inside node.js.yml file.
name: Node.js CI
on:
push:
branches: [master]
jobs:
build:
runs-on: self-hosted
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm i
Github Action Node.js Workflow
name - This is the workflow name appears by default and this is optional.
on - This event triggers the workflow file to run jobs. In our example, we are using push event, so on every push made to the master branch of our repository, the job will run build will be generated. We can set up the workflow to only run on certain branches, paths, or tags.
jobs - Workflows are made up of one or more jobs that can be run on your repository and all the jobs in the GitHub actions will run parallelly by default.
runs-on - The type of machine we would like to run on. We would be using self-hosted runner as we are using an operating system which might not be supported by Github hosted runner. We would also self-hosted hardware configurations on the AWS cloud.
matrix - We would be using matrix to specify which programming language and version will be supported by our Github actions workflow. It also supports the different operating system and we can create a variety of configurations and multiple jobs
steps - Groups all the step to run on our self hosted runner
uses: actions/checkout@v2 - This will checkout the Github code from our repo and pushes the code on our runner to perform further steps/actions on our codebase.
uses: actions/setup-node@v1 - This will install npm on the runner. It won't install npm if it's already cached on the servers.
run: npm i - the run command executes the job on the runner. Here we would be installing all the packages of our project.
- Let's go to the Github settings and click on the actions
- Enable all the actions
- Add the runner
- We can select the operating system and the architecture accordingly. We would go with Linux and ARM64.
- Run the job and we can see that this build will fail currently, as we have not set up our cloud environment to deploy our application and our runners are not connected with the remote host. Let's connect our GitHub actions with our productions servers.
We would be deploying our application on AWS cloud environment, let's create the EC2 instance and configure it ready for our deployments.
- Create an EC2 instance.
We would select the Ubuntu operating system. Visit this article, if you want to learn more about how to create an EC2 instance.
- Review our launch of the EC2 instance
- Create security groups. Make sure the inbound port 80 & 443 is enabled.
Now, let's login to our server to perform activities to get our runner up and running and to connect with GitHub actions self-hosted runner.
ssh -i jenkins_aws_key.pem ubuntu@your-server-public-IP
#login to root user
sudo su -
- Create a new user providing the root privileges
adduser desaijay #this will ask you to set a password and certain questions such as
#Enter the new value or press ENTER for the default
#Full Name []:
#Room Number []:
#Work Phone []:
#Home Phone []:
#Other []:
usermod -aG root desaijay
Log in with the user desaijay
su - desaijay
Now let's run all the below commands to configure and execute the GitHub actions runner, so that our self-hosted runner and listen to our GitHub events/actions/commands. You can get all the commands for your setup on your repository. Click here to get below commands
Download
#Create a folder
mkdir actions-runner && cd actions-runner
#Download the latest runner package
curl -O -L https://github.com/actions/runner/releases/download/v2.273.5/actions-runner-linux-arm64-2.273.5.tar.gz
#Extract the installer
tar xzf ./actions-runner-linux-arm64-2.273.5.tar.gz
Configure
- Create the runner and start the configuration experience. This will ask certain question, only enter the work directory name as express-backend as shown below in the screenshot
./config.sh --url https://github.com/desaijay315/nodejs-github-actions --token ACHKWYI4JIGADETGKLTSH4S7RQ5YA
- Run this as the service!
sudo ./svc.sh install
- Start the service
Now, let's go to the folder where our code for this application resides and install pm2
PM2
PM2 is the process manager for node.js applications that helps you keep the application online. This is logs facility and many salient features. Visit here to learn more about it
npm i -g pm2
Above command might give you some problem if the user permissions are not set properly on the node_modules and we don't want to use sudo to run the above command to install pm2 and if it does so, we can run below commands to minimize permission error issues(Alternatively you can use nexus registry to install all the global node modules)
#create the folder on the home directory
mkdir ~/.npm-global
#configure npm to use the new directory path:
npm config set prefix '~/.npm-global'
#Open profile file with vim and insert below line:
vim ~/.profile
export PATH=~/.npm-global/bin:$PATH
#update the system variables
source ~/.profile
Symlinking pm2
sudo ln -s /home/desaijay/.npm-global/bin/pm2 /usr/bin/pm2
- To start our app with pm2, run the below command
pm2 start src/server.js --name=API
- You must see our application is up and running in the browser for the port 3000.
Web Server - Nginx
Nginx is one of the most popular web servers in the world and is responsible for hosting some of the largest and highest-traffic sites on the internet. It is more resource-friendly and can be used as a web server or reverse proxy.
Now, let us install Nginx and configure it.
sudo apt install Nginx
- Check the status
sudo systemctl status Nginx
- It should be active, and if it's not, you can start Nginx by running below command
sudo systemctl start Nginx
- In an ideal scenario, we would like our APIs to be accessed without the port from the browser and any random port on our machine must not be allowed to access.
- Currently, it's enabled like this - 13.127.99.34:3000/api and we would be accessing our API like this - 13.127.99.34/api.
We would create a custom configuration and rewrite the URL in the location directive and leverage Nginx reverse proxy feature
Add the below server block inside the /etc/nginx/sites-available/default file
sudo vim /etc/nginx/sites-available/default
- Nginx Reverse Proxy + Rewrite URL
location /api {
rewrite ^\/api\/(.*)$ /api/$1 break;
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
We will configure some custom settings on our firewall. We will use UFW (Uncomplicated Firewall) commands and is available by default on ubuntu LTS version greater than 8.04
Check the status of the ufw
sudo ufw status
- If it is disabled, then let's activate the ufw
sudo ufw enable
- Run below commands to enable the Nginx server to take all the requests
ufw app update Nginx
ufw allow 'Nginx Full'
Nginx Full: This profile opens both port 80 (normal, unencrypted web traffic) and port 443 (TLS/SSL encrypted traffic)
- We would be denying the port 3000 as we don't want to keep any ports open on our server and leverage Nginx reverse proxy configuration
sudo ufw deny 3000
- Status should look something like below
- Restart the Nginx server
sudo systemctl restart Nginx
We should now see that services are being proxied by the same server and can be accessed without the port.
References
- Workflow Syntax for GitHub Actions
- Events that trigger workflows
- Managing workflow runs
- Context and Expression syntax for Github Actions
Summary
Overall, my opinion is that GitHub Actions is worth a try. By giving your development teams such tools and mandates to bring operational thinking to their work, organizations can help the team focus on important initiatives and gain endless amounts of benefits.
In this article, we learned how to automate our application deployments with GitHub actions. We learned how to leverage web servers like Nginx.
Until then feel free to connect, and leave your responses below and reach out to me if you face any issues.
You can find me on Twitter