Stafini
BlogFlashcardsProjectsResume

Deploy Web Stack to AWS EC2 - Part 1

Just returning from an intense gaming streak featuring Resident Evil and Marvel's Spiderman 2, culminating with the completion of Marvel's Spiderman Remastered, it's been quite the challenge. Having conquered Resident Evil 3 Remake, I've found a renewed interest in platinum achievements for more games, especially after a two-month job hunt.


Now, let's dive into the main topic – an approach to releasing a web stack to production, employing the following technologies:

  • Vite (Frontend)
  • Express.js (Backend)
  • Postgres (Database)
  • AWS EC2 (VPS instance)
  • pm2 (Task Runner)
  • github (Source Control)
  • nginx (Web server)

For part 1, we will deploy manually, while part 2 will cover the use of Jenkins for automated deployment. I felt the content might be extensive for a single post, so I split it into two parts.

Deploying web stack to AWS EC2 Ubuntu server

Diagram

Illustration of the Interactions between Components using nginx as the Web Server:

1. Setup EC2 instance

Below is a video to demonstrate how to setup EC2 instance, then we can SSH into it to do other installations

2. Install and Configure PostgreSQL

Update packages

sudo apt update && sudo apt upgrade -y  

Install PostgreSQL

sudo apt install postgresql postgresql-contrib -y

Postgres installation will have automatically created a postgres user on Ubuntu as well to allow local connection. this can be verified by running the command:

ubuntu@ip-172-31-18-43:~$ sudo cat /etc/passwd | grep -i postgres
postgres:x:115:123:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash

To connect to Postgres, switch to the postgres user and run psql:

ubuntu@ip-172-31-18-43:~$ sudo -i -u postgres
postgres@ip-172-31-18-43:~$ psql
psql (14.9 (Ubuntu 14.9-0ubuntu0.22.04.1))
Type "help" for help.

postgres=# 

Our Backend connects to database by default postgres user, so we don't need to create another user. But if you need to you can:

postgres@ip-172-31-18-43:~$ createuser --interactive
Enter name of role to add: steam
Shall the new role be a superuser? (y/n) y

Verify the new steam user was created successfully:

postgres@ip-172-31-18-43:~$ psql
psql (14.9 (Ubuntu 14.9-0ubuntu0.22.04.1))
Type "help" for help.

postgres=# \du
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
 steam     | Superuser, Create role, Create DB                          | {}

postgres=# 

Right now the postgres user in Postgres does not have a password associated with it. We will need to add a password:

ubuntu@ip-172-31-18-43:~$ sudo -i -u postgres
postgres@ip-172-31-18-43:~$ psql
psql (14.9 (Ubuntu 14.9-0ubuntu0.22.04.1))
Type "help" for help.

postgres=# ALTER USER postgres PASSWORD 'password';
ALTER ROLE

3. Clone github code to server

Find a place to store your application code. In this example in the ubuntu home directory a new directory called apps will be created. Within the new apps directory another directory called ec2-app. Feel free to store your application code anywhere you see fit

ubuntu@ip-172-31-18-43:~$ cd ~
ubuntu@ip-172-31-18-43:~$ mkdir apps
ubuntu@ip-172-31-18-43:~$ cd apps
ubuntu@ip-172-31-18-43:~/apps$ mkdir ec2-app

Additional steps need to be taken in order to clone github repository with SSH.

Navigate to .ssh folder in ~ path

cd ~/.ssh

Generate key, enter as prompt, remember to leave passphrase empty since we will use this for github-actions later and you can't enter password when actions run the commands

ubuntu@ip-172-31-18-43:~/.ssh$ ssh-keygen -t rsa -b 4096 -C "[email protected]"
Generating public/private rsa key pair.
Enter file in which to save the key (/home/ubuntu/.ssh/id_rsa): github-actions
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in github-actions
Your public key has been saved in github-actions.pub

Create a config file in .ssh folder if it is not existed

cd ~/.ssh
sudo nano config

Add configuration to use github-actions as default key when we pull from repository

Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/github-actions

Start the ssh-agent in the background

eval "$(ssh-agent -s)"
Agent pid 17446

Navigate to .ssh folder again, then open generated .pub

cat github-actions.pub
ssh-rsa xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx [email protected]

Copy content then close editor with Shift + : => :q

Add the key under Github Setting page tab SSH and GPG keys -> New SSH key

4. Install Node

Follow detail steps in:

https://github.com/nodesource/distributions/blob/master/README.md#nodejs

Download and import the Nodesource GPG key

sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg

Create deb repository

NODE_MAJOR=21
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list

Run Update and Install

sudo apt-get update
sudo apt-get install nodejs -y

Check node and npm version

node -v 
v21.1.0
npm -v 
10.2.0

Run npm install in each app folder for Frontend and Backend

cd ~/apps/ec2-app/aws-ec2-sample-fe
npm install && npm run build && npm run dev

➜  Local:   http://localhost:4173/
➜  Network: use --host to expose
➜  press h to show help

cd ~/apps/ec2-app/aws-ec2-sample-be
npm install && npm start

Server running on port 3001
Connected to the database 

5. Install and Configure NGINX

Install and enable nginx

sudo apt install nginx -y
sudo systemctl enable nginx

We can check if nginx works properly by accessing Public IPv4 address which you can get in EC2 instance detail.

Navigate to /etc/nginx/sites-available

cd /etc/nginx/sites-available

There should be a server block called default

ubuntu@ip-172-31-18-43:/etc/nginx/sites-available$ ls
default

The default server block is what will be responsible for handling requests that don't match any other server blocks. Right now if you navigate to your server ip, you will see a pretty bland html page that says NGINX is installed. That is the default server block in action.


We will need to configure a new server block for our website in /etc/nginx/sites-available folder, best practice to name server file as our domain name, so let's create steambyte.dev file since I have one spare domain name from Cloudflare.

cd /etc/nginx/sites-available
sudo touch steambyte.dev

Open steambyte.dev and modify it:

server {
    listen 80;
    listen [::]:80;
    server_name www.steambyte.dev steambyte.dev

    location / {
        # Frontend server
        proxy_pass http://127.0.0.1:4173/;
        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;
    }

    location /api {
        # Backend server
        proxy_pass http://127.0.0.1:3001;
        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;
    }
}

Enable the new site

Enable the Server Block by creating a symbolic link from your configuration file in sites-available to the sites-enabled directory to enable the server block:

sudo ln -sf /etc/nginx/sites-available/steambyte.dev /etc/nginx/sites-enabled/

Test configuration by verify that your nginx configuration is correct by running:

sudo nginx -t

Finally, reload nginx to apply the new configuration:

systemctl restart nginx

It may require you to enter ubuntu password

==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===
Authentication is required to restart 'nginx.service'.
Authenticating as: Ubuntu (ubuntu)
Password: 

Since we have not set on for the account, let's do it by switching to root

ubuntu@ip-172-31-18-43:/etc/nginx/sites-available$ sudo su -
root@ip-172-31-18-43:~# passwd ubuntu
New password: 
Retype new password: 
passwd: password updated successfully

Let's restart nginx again

systemctl restart nginx

Check nginx running status:

ubuntu@ip-172-31-18-43:/etc/nginx/sites-enabled$ systemctl status nginx
● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
     Active: active (running) since Thu 2023-11-09 03:25:34 UTC; 4min 11s ago
       Docs: man:nginx(8)
    Process: 27916 ExecStartPre=/usr/sbin/nginx -t -q -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
    Process: 27917 ExecStart=/usr/sbin/nginx -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
   Main PID: 27918 (nginx)
      Tasks: 2 (limit: 1121)
     Memory: 2.5M
        CPU: 27ms
     CGroup: /system.slice/nginx.service
             ├─27918 "nginx: master process /usr/sbin/nginx -g daemon on; master_process on;"
             └─27919 "nginx: worker process" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" >

Nov 09 03:25:34 ip-172-31-18-43 systemd[1]: Starting A high performance web server and a reverse proxy server...
Nov 09 03:25:34 ip-172-31-18-43 systemd[1]: Started A high performance web server and a reverse proxy server.

6. Install and Configure pm2

We use a process manager like pm2 to handle running our frontend and backend from background. It will also be responsible for restarting the App if/when it crashes.

sudo npm install pm2 -g

Start Frontend and Backend with pm2

# Navigate to frontend folder
pm2 start npm --name fe -- start
┌────┬───────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name  │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├────┼───────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0  │ fe    │ default     │ N/A     │ fork    │ 6764     │ 22s    │ 0    │ online    │ 0%       │ 66.1mb   │ ubuntu   │ disabled │
└────┴───────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

Then do the same for in backend folder

pm2 start npm --name be -- start
┌────┬───────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name  │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├────┼───────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 1  │ be    │ default     │ N/A     │ fork    │ 6814     │ 0s     │ 0    │ online    │ 0%       │ 19.8mb   │ ubuntu   │ disabled │
│ 0  │ fe    │ default     │ N/A     │ fork    │ 6764     │ 22s    │ 0    │ online    │ 0%       │ 66.1mb   │ ubuntu   │ disabled │
└────┴───────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

To ensure that PM2 processes start automatically upon an EC2 instance reboot, you can set PM2 as a startup script. On Ubuntu, you can use the following steps:

First, save the current PM2 configuration:

pm2 save

Then, generate the startup script:

ubuntu@ip-172-31-18-43:~/apps/ec2-app/aws-ec2-sample-be$ pm2 startup
[PM2] Init System found: systemd
[PM2] To setup the Startup Script, copy/paste the following command:
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u ubuntu --hp /home/ubuntu

Then run the script

sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u ubuntu --hp /home/ubuntu

Finally, to ensure your current processes are resurrected on startup, run:

pm2 save

This will set up PM2 to restart your Node.js applications managed by PM2 in case of any server reboots.

Make sure to replace the placeholders with the actual paths and adjust the configurations according to your specific requirements and file structures.

7. Config Cloudflare DNS to EC2 instance

Copy Public IPv4 address from your EC2 instance

Open DNS Configuration for your domain, then add 2 record for example.com and www which point to the IPv4 we just copied from EC2

8. Enable Firewall (optional)

sudo ufw status
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw enable
sudo ufw status

9. Enable SSH with Let's Encrypt (optional)

Nowadays almost all websites use HTTPS exclusively. Let's use Let's Encrypt to generate SSL certificates and also configure NGINX to use these certificates and redirect http traffic to HTTPS.

The step by step procedure is listed at: https://certbot.eff.org/lets-encrypt/ubuntufocal-nginx.html

Install Certbot

sudo snap install --classic certbot

Prepare the Certbot command

sudo ln -s /snap/bin/certbot /usr/bin/certbot

Get and install certificates using interactive prompt

sudo certbot --nginx

10. Go Live

Live site at https://steambyte.dev/ showcasing basic functionalities, emphasizing our demonstration of the AWS EC2 deployment workflow in this post. Stay tuned for our next post, focusing on automatic deployment, eliminating the need for manual pm2 restarts during deployments.