Automate Deployment of Node.js App on AWS EC2 via GitHub Actions CI/CD Pipeline

In this guide, we'll walk you through the process of setting up automatic deployment for a Node.js REST API on an AWS EC2 instance using a CI/CD pipeline with GitHub Actions. You'll learn how to configure your EC2 instance, set up your GitHub repository, and create workflows that ensure your app is deployed seamlessly whenever you push changes to your main branch. Let's get started!

Prerequisites

  1. AWS Account: Ensure you have an AWS account and proper permissions to create and manage EC2 instances.

  2. GitHub Account: Ensure you have a GitHub account and a repository for your Node.js project.

  3. Node.js and NPM: Ensure you have Node.js and NPM installed on your local machine.

1. Setup Local Node.js Project

1.1 Initialize Node.js Project

  • Init your NodeJS app and install its dependencies using the following command:

      npm init -y
      npm install express dotenv
    
  • Create a .env file in your project with the port number specified:

      PORT = 5000
    
  • Create index.js file and set a basic express web server:

      require('dotenv').config();
      const express = require('express');
      const app = express();
      const port = process.env.PORT;
    
      app.get('/', (req, res) => {
      res.status(200).send(`Hello, from Node App on PORT: ${port}!`);
      });
    
      app.listen(port, () => {
      console.log(`App running on http://localhost:${port}`);
      });
    

    This Node.js script creates a simple Express server that listens on a port specified in a .env file. When accessed at the root URL (/), it responds with a message showing the port number. The server starts and logs a message indicating it's running and the port it's using.

1.2 Upload it in a GitHub Repository

  1. Create GitHub Repository: Create a new repository for your Node.js project on GitHub. In our case, we named it NodeJS-App-in-EC2.

  2. Push Code to GitHub: Initialize Git, add remote origin and push your code to the GitHub repository.

2. Setup AWS EC2 Instance

Login to your AWS account and continue with the following process:

2.1 Launch EC2 Instance

  1. Choose "Ubuntu" as the operating system.

  2. Ensure it's a free-tier eligible instance type.

  3. Create a new key pair or use an existing one.

2.2 Configure Security Group

  1. Go to security group > select the one for our instance > Edit inbound rules.

  2. Allow SSH (port 22) from your IP.

  3. Allow HTTP (port 80) from anywhere.

3. Connect to EC2 Instance

3.1 SSH into EC2 Instance

  1. Go to your instance and click on connect.

  2. Open the terminal and navigate to the directory with your PEM file.

  3. Use the SSH command provided by AWS to connect. We used Windows Powershell to SSH into the EC2 Instance.

4. Setup GitHub Actions Runner on EC2

  1. Go to your repository’s settings on GitHub.

  2. Under “Actions”, click “Runners” and add a new self-hosted runner for Linux.

  3. Follow the commands provided to set up the runner on your EC2 instance.

    You will get something like this after the final command (marked portion):

    After that, keep hitting Enter to continue with the default settings. Now if you go to the GitHub repository > settings > runners, you will get something like this:

    It is in offline state. Go to the SSH PowerShell and use the following command:

     sudo ./svc.sh install
     sudo ./svc.sh start
    

    Now the runner in the GitHub repository is no more in Offline state:

5. Create GitHub Secrets

  • Go to your repository’s settings.

  • Under “Secrets and variables” > “Actions”, create a New repository secret with your .env variables. In our case, we named it ENV_FILE. File Content:

      PORT = 5000
    

    Save the secret.

6. Setup CI/CD Pipeline with GitHub Actions

6.1 Create GitHub Actions Workflow

  • In your repository, go to the “Actions” tab.

  • Choose the Node.js workflow template.

  • Configure/Change it as follows:

      name: Node.js CI/CD
    
      on:
      push:
          branches: [ "main" ]
    
      jobs:
      build:
          runs-on: self-hosted
          strategy:
          matrix:
              node-version: [18.x]
    
          steps:
          - uses: actions/checkout@v4
          - name: Use Node.js ${{ matrix.node-version }}
          uses: actions/setup-node@v3
          with:
              node-version: ${{ matrix.node-version }}
              cache: 'npm'
    
          - run: npm ci
          - run: |
              touch .env
              echo "${{ secrets.ENV_FILE }}" > .env
    

    This GitHub Actions configuration sets up a CI/CD pipeline for a Node.js application. The workflow triggers a push to the main branch. It runs on a self-hosted runner and uses the Node.js version 18.x. The steps include checking out the code, setting up Node.js, and installing dependencies using npm ci, and creating a .env file from a secret stored in GitHub (ENV_FILE that we've created).

6.2 Deploy and Verify

  1. Push Changes to GitHub: Commit and push changes to your GitHub repository.

  2. Check GitHub Actions: Ensure the workflow runs successfully and deploys the updated code to your EC2 instance.

    If you go to the SSH terminal, you can see _work directory. Now go to _work folder. In this folder, you will see your Nodejs app directory from Git Hub. Therefore code is updated to your EC2 instance.

7. Install Node.js and Nginx on EC2

Now go to _work folder. In this folder, you will see your Nodejs app directory from Git Hub. Navigate to that directory. Use ls to see your app files.

  1. Update Packages:

     sudo apt-get update
    
  2. Install Node.js and NPM:

     sudo apt-get install -y nodejs
     sudo apt install -y npm
    

    Check Node and NPM versions:

     node -v
     npm -v
    
  3. Install Nginx:

     sudo apt-get install -y nginx
    

    If you visit http://<ec2-instance-public-ip> you will see:

  4. Install pm2:

     sudo npm i -g pm2
    

    The command installs PM2 globally on your system with superuser (root) privileges. PM2 is a popular process manager for Node.js applications, which helps manage and run applications.

    Verify pm2 installation:

     pm2
    

8. Configure Nginx

  1. Edit Nginx Config:

     cd /etc/nginx/sites-available
     sudo nano default
    
  2. Add Reverse Proxy Configuration: Set the server block as follows.

     server {
         listen 80;
         server_name _;
    
         location / {
          proxy_pass http://localhost:5000;
          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;
    
          # Additional headers for proper proxying
          proxy_set_header X-Real-IP $remote_addr;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header X-Forwarded-Proto $scheme;
      }
     }
    
  3. Restart Nginx:

     sudo systemctl restart nginx
    

    If you visit http://<ec2-instance-public-ip> you will see:

    It gives an error because our NodeJS app in ec2 is not running.

  4. Start the NodeJS App:

    Let's navigate to the NodeJS app directory and run the server using the following command:

     pm2 start index.js
    

    Expected output:

     ubuntu@ip-172-31-30-252:~/actions-runner/_work/Github-Actions-NodeJS-App/Github-Actions-NodeJS-App$ pm2 start index.js
    
     [PM2] Spawning PM2 daemon with pm2_home=/home/ubuntu/.pm2
     [PM2] PM2 Successfully daemonized
     [PM2] Starting /home/ubuntu/actions-runner/_work/Github-Actions-NodeJS-App/Github-Actions-NodeJS-App/index.js in fork_mode (1 instance)
     [PM2] Done.
     ┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
     │ id │ name     │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
     ├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
     │ 0  │ index    │ default     │ 1.0.0   │ fork    │ 88430s     │ 0    │ online    │ 0%       │ 39.0mb   │ ubuntu   │ disabled │
     └────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
    

    Note that the process name is index which can be different in your case.

    Now, if you visit http://<ec2-instance-public-ip> you will see:

    Our App has been successfully deployed to AWS and Nginx is serving our NodeJS app properly.

9. Verify CI/CD

9.1 Update the Workflow manifest

Go to your GitHub repository. From the workflows directory open the node.js.yml and start editing. Add - run: pm2 restart index at the end so that our NodeJS app restarts automatically whenever there is a new deployment. Here index is the process name we saw earlier which can be different in your case. Final node.js.yml will be:

name: Node.js CI/CD

on:
  push:
    branches: [ "main" ]

jobs:
  build:
    runs-on: self-hosted
    strategy:
      matrix:
        node-version: [18.x]

    steps:
    - uses: actions/checkout@v4
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'

    - run: npm ci
    - run: |
        touch .env
        echo "${{ secrets.ENV_FILE }}" > .env
    - run: pm2 restart index

9.2 Deploy and Verify

  1. Push Changes to GitHub: Commit and push changes to your GitHub repository.

  2. Check GitHub Actions: Ensure the workflow runs successfully and deploys the updated code to your EC2 instance.

9.3 Update the local NodeJS app

Update the index.js of your app locally:

require('dotenv').config();
const express = require('express');
const app = express();
const port = process.env.PORT;


app.get('/', (req, res) => {
  res.status(200).send(`Hello, from Node App on PORT: ${port}!`);
});

app.get('/users', (req, res) => {
    res.status(200).send(`Congratulations! You have reached the users endpoint!`);
});

app.listen(port, () => {
  console.log(`App running on http://localhost:${port}`);
});

Here we have added new /users endpoint. Let's push the code in github to see the updated app in our aws ec2 instance. We need to pull first. Use the following command:

git pull
git add .
git commit -m "Added /users endpoint"
git push

Now if we go to the github action tab we will see new workflow triggered by our new commit with same name as our commit message.

Now, if you visit http://<ec2-instance-public-ip>/users you will see:

So the new endpoint has been automatically added to our AWS deployment.

Additional Tips

  • Environment Variables: Use GitHub secrets to securely manage environment variables.

  • Security: Ensure your EC2 instance is secure and only accessible from necessary IPs.

  • Monitoring: Use tools like PM2 for process management and monitoring.

Conclusion

This guide covers the setup of a CI/CD pipeline to automatically deploy a Node.js application on an AWS EC2 instance using GitHub Actions. By following these steps, you can automate your deployment process, ensuring consistent and reliable updates to your application.

0
Subscribe to my newsletter

Read articles from Mohammad Minhazul Alam directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mohammad Minhazul Alam
Mohammad Minhazul Alam