Deploy your Node App to EC2 with Github Actions
Tags: aws, devops, node, javascript
Published: 2021-12-10 18:00:30

In this post we are going to run through how to deploy our code to AWS EC2 via Github Actions. This will include the following steps:

1. [Create EC2 instance](#create-ec2)
2. [Configure IP and domain name](#routing)
3. [SSH into our server](#ssh-into-server)
4. [Install NGINX](#install-nginx)
5. [Install Node and PM2](#install-node)
6. [Deploy with Github Actions](#github-actions)

### Create EC2 instance <a name="create-ec2"></a>
We are going to need to launch a new instance, create an SSH key pair and configure the correct security groups.

I wont bore you with the detail of going through the AWS EC2 creation wizard but you can go ahead and create a new EC2 instance with an **Ubuntu** machine image with a t2 micro and download your new keys.

Make sure you download your ssh keys and store them somewhere secure as you will need them again later.

![ssh-key-selection](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hy2ad6ijwt35kgkjd2rn.png)

We have created our new instance so whilst it is booting up, we are going to configure a security group. This will allow us to accept incoming and outgoing network reqeusts on specific ports and IP addresses.

Go to the Security Groups page and then select "Create security group".

![security-group-selection](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a54g6gqb7803vnky03wt.png)

We are going to allow `HTTP` (port 80) and `HTTPS` (port 443) on both our incoming and outgoing connections. This means we can access our app via http or https and we can call API's from our app on both those protocols too. Keep in mind ssh (port 22) is enabled by default.

If your app is connecting to a database, you may want to allow rules for PostgreSQL or MySQL etc. 

![securtiy-group-settings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hof7vvx6ppzpn2qji1x1.png)

### Configure IP and domain name <a name="routing"></a>
Here we will assign an IP address to our EC2 instance and then update our domain's name server to point the domain name to our new IP.

Within the EC2 service section of AWS, navigate to Elastic IPs. Here we can click "Allocate Elastic IP address" to generate a new IP address. We can now assign that to the newly created EC2 instance we just created by clicking actions > Associate elastic IP address and selecting our EC2 instance. 

![alt text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u7xr5yetfkgx5xruhf6u.png)

Next we will point our domain name to our new IP address. In my case, my domain name uses AWS Route53 as a name server so I will navigate there and create an A record to point `<my_ip_address> -> www.my-website.com`.

### SSH into our server <a name="ssh-into-server"></a>

Next up we are going to access our EC2 instance via SSH. In oder to do this, we will do the following:

1. Open an SSH client.
2. Locate your private key file
3. Run this command, if necessary, to ensure your key is not publicly viewable.
    ```bash
    $ chmod 400 My-ssh-key.pem
    ```
4. Connect to your instance using its Public DNS:
    ```bash
    $ ssh -i "My-ssh-key.pem" ubuntu@<host>.compute.amazonaws.com
    ```

You should now have SSH access to your EC2 server.

### Install NGINX <a name="install-nginx"></a>

After ssh'ing into our server, we will need to do the following; install NGINX, configure firewall, forward web traffic ports and lastly set directory permissions.

```bash
$ sudo apt update
$ sudo apt install nginx
```

We are then going to adjust our firewall to allow http and https connections to our NGINX server.

```bash
$ sudo ufw allow 'Nginx Full'
```

We should then have an active web server:
```bash
$ systemctl status nginx
```
![nginx-status](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/plh620jdqtklh8v52lqi.png)

If we visit `http://<my_ip_address>` we should see the NGINX welcome page.
![nginx-welcome-page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bwpj8eg68w90hmfftcyj.png)

We now know our web server is accepting connections on port 80. Our Node app is going to be running on port `3000` so we are going to need to forward connections from port `80` to `3000`. This will be done via our NGINX config in the sites-available file.

```bash
$ sudo nano /etc/nginx/sites-available/my-app
```
We are going to paste in this config:
```bash
upstream my_nodejs_upstream {
    server 127.0.0.1:3000;
    keepalive 64;
}

server {
    listen 80;
    # listen 443 ssl;
    
    server_name www.my-website.com; # This is the domain name we set up earlier to point to our IP address
    # ssl_certificate_key /etc/ssl/main.key;
    # ssl_certificate     /etc/ssl/main.crt;
   
    location / {
    	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
    	proxy_set_header Host $http_host;
        
    	proxy_http_version 1.1;
    	proxy_set_header Upgrade $http_upgrade;
    	proxy_set_header Connection "upgrade";
        
    	proxy_pass http://my_nodejs_upstream/;
    	proxy_redirect off;
    	proxy_read_timeout 240s;
    }
}
```

This listens for traffic for `www.my-website.com` (this will be the domain you set up earlier to point to your IP address) on port 80 and forwards it to our node app on port 3000. As you can see, I have commented out the SSL config but if you have an SSL cert, feel free to use that instead.

We are now going to need to symlink this file into sites-enabled directory.

```bash
$ sudo ln -s /etc/nginx/sites-available/my-app /etc/nginx/sites-enabled
```

We can go ahead and restart NGINX to make all these changes take effect.

```
$ sudo systemctl restart nginx
```

Lastly for this step, we are going to create a file for our application files to live in and set the correct permissions.

```bash
$ sudo mkdir /var/www/my-app
```

Change the directory owner and group:
```bash
$ sudo chown www-data:www-data /var/www/my-app
```

Allow the group to write to the directory with appropriate permissions:
```bash
$ sudo chmod -R 775 /var/www
```

Add myself to the www-data group:
```bash
$ sudo usermod -a -G www-data [my username]
```

We should now have NGINX installed with a config that forwards incoming requests on port 80 to port 3000 and have a directory created to contain our site files with all the correct permissions.


### Install Node and PM2 <a name="install-node"></a>

We are going to install Node via [NVM](https://github.com/nvm-sh/nvm) so that we can easily change node versions and then we will use `npm` to install `pm2`.

1. To install `nvm`:
    ```bash
    $ curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash 
    $ source ~/.profile 
    ```
2. Install the version of Node you want
    ```bash
    $ nvm install 14
    ```
3. We should now have Node and NPM installed which we can now use to install PM2.
    ```bash
    $ npm install pm2 -g
    ```

If we had our code deployed to our server, we would then be able to start our app via PM2 but we will have to come back to this once our CI pipeline is complete.

### Deploy with Github Actions <a name="github-actions"></a>

Now we can finally create a deployment pipeline in Github actions that automatically deploys our code to our new server (into the directory we created with the correct permissions). We will then move back to our ssh terminal in order to start our app.

Before we deploy anything, we are going to upload our previously downloaded ssh private key, into Github secrets so that we can reference it in our CI pipeline.

We are going to name ours `SSH_PRIVATE_KEY`:

![github-actions-secret-settings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/na6h3uqlwncbvfyqc7bo.png)
 

Using the following yaml file, we do the following:
1. install dependencies
2. run build
3. run tests
4. use rsync to copy files to our server (it will replace existing files)

```yaml
name: CI

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

  workflow_dispatch:

jobs:
  build:

    runs-on: ubuntu-latest

    if: github.ref == 'refs/heads/master'

    strategy:
      matrix:
        node-version: [14.x]

    steps:
      - uses: actions/checkout@v2

      - run: npm ci
      - run: npm run build
      - run: npm run test

      - name: rsync deployments
        uses: burnett01/rsync-deployments@5.1
        with:
          switches: -avzr --delete
          path: ./*
          remote_path: /var/www/my-app/
          remote_host: <host>.compute.amazonaws.com
          remote_user: ubuntu
          remote_key: "${{ secrets.SSH_PRIVATE_KEY }}"
```

Push this to our master branch in our Github repo and we should see an action run. If successful, our files should now be on our server in `/var/www/my-app`.

If we switch back to where we're ssh'd into our server, we can now start our app.

```bash
$ pm2 start /var/www/my-app/dist/index.js start --watch
```
* In my case, when I run `npm run build` it outputs my built files into the `dist` directory. You may have a different path to where you start your app from. 

* Also, by adding `--watch` we allow pm2 to restart the service whenever the files change. I.e. after each deployment. 

* If you need to set any environment variables, you can do so like so (keep in mind you will need to restart your app afterwards).
    ```bash
    $ export MY_ENV_VAR=foo_bar
    ```

We should now have successfuly setup NGINX, installed node (via NVM) and PM2 and setup a deployment pipeline in Github actions.

With any luck, our application should be available to the public via  `http://www.my-website.com` or whatever domain name you configured.

Shout out in the comments if you have any questions or issues following this guide and I'll do my best to answer them. 

Happy coding and look forward to any any feedback.