Skip to content

Instantly share code, notes, and snippets.

@linuxdevops-34
Forked from rmiyazaki6499/deploy-mern.md
Created July 17, 2020 04:40
Show Gist options
  • Save linuxdevops-34/253b185e0718bc368cf0792915299710 to your computer and use it in GitHub Desktop.
Save linuxdevops-34/253b185e0718bc368cf0792915299710 to your computer and use it in GitHub Desktop.
Deploying a Production ready React-Express app on AWS EC2 with CI/CD

Deploying a Production ready React-Express App on EC2 with CI/CD

In this guide I will go through the steps of setting up your EC2 instance for your React-Express project and deploy it with CI/CD using Github Actions.

Any commands with the "$" at the beginning run on your local machine and any with "%" should be run on your server.

Table of Contents


Project Layout

If you already have a working project go ahead and move on to either Creating your AWS Account or Creating your EC2 Instance.

Otherwise feel free to use the generic MERN project I created on Github here.

Here is the project layout:

mern-app
 |__ client/ (React App Frontend)
    |__ public/
    |__ src/
 |__ app.js (Express Backend)
 |__ package.json


Setting up the project

I will be using a generic MERN (MongoDb, Express, React, Node.js) stack app which uses a proxy with the Express server. The app simply displays the default React app components however, I have added a simple API which the frontend calls to confirm that the API call is successfull.

Start by cloning the project with the command:

$ git clone https://github.com/rmiyazaki6499/mern-app.git

Next, we will install dependencies for both Express and React:

$ cd mern-app
$ npm install
$ cd client
$ npm install

Let's first check to see what our React frontend looks like. To run the React server use the command in client directory:

$ npm start

Now if you go to http://localhost:3000, you should see something like this:

mern-app_react

The API is not working because well, we are not running our backend yet! Let's do that now.

In another terminal session run the command npm start at the root directory of the project as we did with the frontend. It should look something like this:

mern-app_run_server

You can see that we have the express server running on port 5000.

Now switch back to the http://localhost:3000 and refresh the page. You should see the Message at the bottom be updated!

mern-app_react_success

We have two servers running, one for the React frontend and one for the Express backend. If your project set up is essentially two separate projects between the frontend and backend this would be as far as we would need to check. However, I have set this project set up so that rather than running two servers, we run a reverse proxy for React through Express and will serve the frontend through the Express server.

Because we will not be running the React server for our project, go ahead and stop the React server.

In the client directory run the command:

$ npm run-script build

React then will create a build directory with a production build of your app which is where our Express server will use to serve the frontend.

Now if you go to http://localhost:5000 you should see the same React page from earlier!

Back to Table of Contents


Create an AWS Account

Before creating an EC2 Instance, you will need an AWS account.

If you don't have one already check out this step by step by Amazon to create your AWS account here.

Before we move on to create an EC2 instance, make sure that you are making your account as secure as possible by following the prompt on your Security Status checklist.

security_status


Create an AWS EC2 Instance

Once you have set up your user account we will jump in to creating our first EC2 Instance.

Note: This tutorial assumes you have at least access to the AWS Free Tier products

Amazon's EC2 or Elastic Compute Cloud is one of the products/services AWS provides and is one of the main building blocks for many of AWS's services. It allows users to rent virtual computers on which to run their own computer applications.

You can learn more about EC2 here.

Start out by going into the AWS Console and going to the EC2 console. An easy way to get there is through the Services link at the top and searching EC2 in the search prompt.

We recommend setting your AWS Region to the one closest to you or your intended audience. However, please note that not all AWS Services will be available depending on the Region. For our example, we will be working out of the us-east-1 as this Region supports all AWS Services.


EC2 Console

You should end up at a screen which looks like this (As of July 2020).

ec2_console

Go to the Running Instances link on the EC2 Console Dashboard to find yourself at this screen.

ec2_running_instances


AMI

We will then go to Launch Instance which will give you several prompts.

AWS will first ask you to choose an AMI. If you do not already have an AMI set up choose an OS that you would like to work in. For our example, we will use Ubuntu 18.04 with 64-bit.

ec2_choose_ami

Next we will choose an instance type. We will choose the t2.micro type as it is eligible for the free tier.

ec2_choose_instance_type

Once selected, click forward to Next: Configure Security Group.


Security Groups

This is important! Without configuring Security groups the ports on the instance will not be open and therefore your app will not be able to communicate through your instance.

Set your Security Group Setting like so:

ec2_security_group_configuration

I will explain each of the ports we will allow as the firewall rules for traffic.

Type Port Range Description.
SSH 22 Port for SSH'ing into your server
HTTP 80 Port for HTTP requests to your web server
HTTPS 443 Port for HTTPS requests to your web server
Custom TCP 5000 Port which Express will run
Custom TCP 27017 Port at which to connect to MongoDB

As you can see with the Warning near the bottom that you do not want to set your Source as Anywhere. Make sure to set it to your IP address or any IP which will need access to the instance. I have this setting so that I do not show my IP address.


Instance Details

Click forward to Review and Launch to view all configurations of your Instance/AMI. If the configurations look correct go ahead and hit Launch.


Key Pairs

A key pair consists of a public key that AWS stores, and a private key file that you store. Together they allow you to connect to your instance securely.

If this is the first time for you to create a key pair for your project, select Create a new key pair from the drop down and add the name of the key pair.

key_pair

Once you have downloaded the key pair make sure to move the .pem file to the root directory of your project.

mern-app_root_w_pem

Make sure to check the checkbox acknowledging that you have access to the private key pair and click Launch Instances.

This should take you to the Launch Status page.


Accessing your EC2 Instance

Find your way back to the Instances page un the EC2 Dashboard. It should look something like this:

ec2_instance_first_initializing

If you go to your instance right after launching the instance may still be initializing. After a few minutes, under the Status Checks tab, it should show 2/2 checks... If it does, congratulations! You have your first EC2 Instance.


Elastic IP

Before we access our EC2 Instance it is important to first receive an Elastic IP and Allocate it to our EC2 instance.

An Elastic IP is a dedicated IP address for your EC2 instance. This is important because although our instance does have an IP address assigned out of the box, it does not persist. With an Elastic IP address, you can mask the failure of an instance or software by rapidly remapping the address to another instance in your account.

Therefore by using an Elastic IP we can have a dedicated IP to which users from the internet can access your instance.

Note: If you are using the free tier, AWS will charge you unless your EC2 Instance is allocated to an Elastic IP.

On the EC2 Dashboard look under the Network & Security tab and go to Elastic IPs.

elastic_ips_link

It should take you here:

elastic_ip_addresses

Click on Allocate Elastic IP address.

It should take you here:

allocate_ip_address

Go ahead and click Allocate.

This should create an Elastic IP for you.

elastic_ip_created

Next we must allocate our Elastic IP to our instance.

With the Elastic IP checked on the left side.

  • Go to Actions
  • Click on Associate Elastic IP address
  • Make sure your Resource type is Instance
  • Search for your instance (if this is your first time, it should be the only one)
  • Click Associate

Let's check to make sure our Elastic IP is Associated with our instance.

Go to Instances and in the instance details you should see Elastic IP: .


Connecting to your EC2 Instance

Now that we have our instance and have allocated an Elastic IP to it. It is time to connect to our server!

If you have not already, go to the Instances link in the EC2 Dashboard.

With the instance highlighted, click on Connect on the top banner of the Instaces Dashboard.

It should give you a pop up with directions on how to connect to your EC2 instance.

connect_to_your_instance

Go back to your project root directory and make sure that your .pem file has the correct permissions.

Run the command:

$ chmod 400 *.pem

Next run the command given to you in the example:

$ ssh -i "<KEY-PAIR>.pem" ubuntu@<YOUR-IP-ADDRESS>.compute-1.amazonaws.com

The ssh should prompt you that the authenticity of host instance can't be established and will show an ECDSA key fingerprint. It will also ask you Are you sure you want to continue connecting (yes/no)?

Type yes and Enter.

This should take you into the EC2 Instance. If not, try the ssh command again.

Congratulations you are inside your EC2 Instance!


EC2 Environment Setup

You can think of our EC2 instance like a brand new server. There is nothing besides the Operating System and a few other things AWS has added into our instance.

Setup Web Server

Before we start building our project there are a few things we must install/configure on our empty server. We will use the following technologies:

  • Node.js 10.x & NPM
  • MongoDB 4.0
  • PM2
  • NGINX
  • UFW (Firewall)

There is a script that will install and configure almost everything for you on an Ubuntu 18.04 server courtesy of Jason Watmore. *If you would like to better understand what is going on in this script please check out his blog here.

curl https://gist.githubusercontent.com/cornflourblue/f0abd30f47d96d6ff127fe8a9e5bbd9f/raw/e3047c9dc3ce8b796e7354c92d2c47ce61981d2f/setup-nodejs-mongodb-production-server-on-ubuntu-1804.sh | sudo bash

Setting up our project

Let's begin to build our project.

We will first start by cloning our project into the EC2 instance with:

% git clone https://github.com/rmiyazaki6499/mern-app.git

We will then start building the project as we would locally.

Installing dependencies for Express:

% cd mern-app
% npm install

Installing dependencies for React and building our project:

% cd client/
% npm install
% npm run-script build (or npm build if you have that set up)

This will then create the build/ directory which will contain the static files Express will use for the frontend.


Starting PM2

PM2 is an Advanced process manager for production Node.js applications which includes functionality like Load Balancing, etc. If we had an API it would run the backend API as well as make sure our processes are running even in case something happens and our app crashes.

Our previous script already downloaded and started PM2 for us but we will need it to run our backend as well as add some options for better performance.

To take a look at our current PM2 processes, using the command:

% sudo pm2 status

You can see that we do not have any processes running yet. At the root of your project directory with our Express app run:

% sudo pm2 start app.js

Note: We are using app.js for our app but yours may use server.js.

Great! Our PM2 is running our backend, but let's stop it for just a second.

% sudo pm2 stop app.js

Let's re-run the process with this command:

% sudo pm2 start app.js -i max --watch

What difference did this make? The -i max option allows us to run processes with the max number of threads available. Because Node is single threaded, using all available cores will maximize performance. The --watch option allows the app to automatically restart if there are any changes to the directory. You can think of it as similar to the package nodemon but for production.


Configuring NGINX

We have our project now but...we can't see or access it...

That is why we will need to configure NGINX, our Web Server so that we can access it.

Something we have to understand before we proceed is that because our project uses two different frameworks. Which essentially means we have two different projects. Therefore there is a need for us to configure NGINX so that depending on route it knows to direct the request to either the frontend (React) or the backend (Express) portion of the app.

For my generic project, I did not create a backend API endpoint but may in the future reiterate this point.

To create a new config file and configure NGINX, use the command: (You can use either nano or vim, I personally use vim):

% sudo vim /etc/nginx/sites-available/<YOUR-PROJECT-NAME>

Add this into your config file and replace any of the ALL CAPS Sections with your own details:

server {
server_name <YOUR EC2 ELASTIC IP ADDRESS>;

# react app & front-end files
location / {
  root /home/ubuntu/<YOUR PROJECT DIRECTORY>/client/build/;
  try_files $uri /index.html;
}

# node api reverse proxy // the /api/ is assuming your api routes start with that i.e. www.your-site.com/api/endpoint
location /api/ {
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header Host $http_host;
  proxy_set_header X-NginX-Proxy true;
  proxy_http_version 1.1;
  proxy_pass http://localhost:5000;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection 'upgrade';
  proxy_set_header Host $host;
  proxy_cache_bypass $http_upgrade;
}
}

We will then enable the config file by linking to the sites-enabled directory. This is important because otherwise NGINX will simply use the default configuration settings located at /etc/nginx/sites-available/default.

% sudo ln -s /etc/nginx/sites-available/<YOUR-PROJECT-NAME> /etc/nginx/sites-enabled

A couple of important points. The root line in the Frontend section needs to be the build directory of your React app. So for our case it would be home/ubuntu/mern-app/client/build/. Make sure the path is correct as this will be where your static files will be served.

The proxy_pass in the backend is the route of where your Express will be running.

Note:For the mern-app project this should be http://localhost:5000 but this can a different port depending on your configurations.

Here is an example for the server name line in the config file: Note: You do not need the http:// portion of the IP.

server_name 18.XXX.XXX.XXX

Save and exit the file: vim: Shift + zz nano: ctrl + x and selecting Yes

Once your NGINX config is set up.

Make sure there are no syntax errors with:

% sudo nginx -t

Restart the NGINX Web Server with:

% sudo systemctl restart nginx

Now if you go to your Elastic IP on your browser it should show the App!

Back to Table of Contents


Setting up Continuous Deployment

Continuous Deployment is helpful because it saves you the time of having to ssh into your EC2 instance each time you make an update on your code base.

We will be using Github Actions and AWS SSM Send-Command created by peterkimzz to implement auto deployment.

Activate SSM Agent

For Github Actions to be able to work, it needs a way for it to communicate to our EC2 Instance when there is a push to the master branch of our repo. There are ways of utilizing, for example webhooks with something like Jenkins to communicate to our server. But for this example, we will use an SSM Agent as a means of connecting Github Actions with our EC2 instance.

You can think of an SSM Agent as a "back door" to our instance and is something that comes by default to most EC2 instances (I believe Ubuntu and Linux instances have it built in, not sure of the others). Even though it is pre-installed, we need to assign an IAM Role to our instance to allow it to have access to SSM.


Create SSM Role

To create an IAM Role with AmazonSSMFullAccess permissions:

  • Open the IAM console at https://console.aws.amazon.com/iam/.
  • In the navigation pane, choose Roles, and then choose Create role.
  • Under Select type of trusted entity, choose AWS service.
  • In the Choose a use case section, choose EC2, and then choose Next: Permissions.
  • On the Attached permissions policy page, search for the AmazonSSMFullAccess policy, choose it, and then choose Next: Review.
  • On the Review page, type a name in the Role name box, and then type a description.
  • Choose Create role. The system returns you to the Roles page.

Assign SSM Role to EC2 Instance

Once you have the Role created:

  • Go to the EC2 Instance Dashboard
  • Go to the Instances link
  • Highlight the Instance
  • Click on Actions
  • Instance Settings
  • Attach/Replace IAM Role
  • Select the SSM Role you had created earlier
  • Hit Apply to save changes

With this your EC2 Instance has access to SSM!


Github Secrets

With our instance being able to use the SSM Agent, we will need to provide it some details so that it can access our EC2 instance.

This would be provided as Github Secrets. They act like environment variables for our project which is useful because we do not want anyone seeing our Secrets publicly anywhere!

There are three Secrets we will need: AWS_ACCESS_KEY, AWS_SECRET_ACCESS_KEY, and INSTANCE_ID.

Before we start, there is an article by AWS on how to find your AWS Access Key and Secret Access Key here.

Start by going to your Github project repo:

  • Then go to your Settings
  • On the menu on the left, look for the link for Secrets
  • There, add the three Secrets with these keys:
    • AWS_ACCESS_KEY_ID
    • AWS_SECRET_ACCESS_KEY
    • INSTANCE_ID

Once these Secrets are set, we are ready to move on!


Deployment script

This step is an extra step mainly to help make things easier although we can do everything in the next step. We will create a bash script which we would have the SSM run with the Github Action is triggered.

Go to your EC2 instance and at the root of your project, create a .sh script:

% vim deploy.sh

Fill the contents with the bash commands we ran to build the project earlier:

#!/bin/sh     
git pull origin master
npm install
cd client
npm install
npm run-script build
cd ..
sudo systemctl restart nginx
sudo pm2 restart all

I will walk through step-by-step what we are doing at each command:

  1. git pull origin master makes sure we have the most up-to-date code which was triggered by a commit to the master branch.
  2. npm install installs any new dependencies for the Express side of the app.
  3. cd client we move into the client directory where our React app lives.
  4. npm install install any new dependencies for the React side of the app.
  5. cd .. return to the root of the project.
  6. sudo systemctl restart nginx restarts nginx so that it is serving the most recent static files.
  7. sudo pm2 restart all resets PM2 so that it knows to recognize any changes to the backend (This might be unnecessary because if you were following along, we set PM2 to be --watch which automatically recognizes any chances).

Now that we have a deployment script we are ready for the last part where we define our .yml file!


yml File

AWS SSM Send-Command requires us to create a .yml file to execute.

Start by going into your EC2 instance and at the root of your project create these two directories:

% mkdir -p .github/workflows/

This is where our .yml file will live. Create the file with:

% sudo vim .github/workflows/deploy.yml

Use the example to fill the contents of the .yml file

name: Deploy using AWS SSM Send-Command 

on:
    push:
        branches: [master]

jobs:
    start:
        runs-on: ubuntu-latest 

        steps:
            - uses: actions/checkout@v2

            - name: AWS SSM Send Command
              uses: peterkimzz/aws-ssm-send-command@1.0.1
              with:
                  aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID  }}
                  aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY  }}
                  aws-region: us-east-1
                  instance-ids: ${{ secrets.INSTANCE_ID  }}
                  comment: Deploy the master branch
                  working-directory: /home/ubuntu/<YOUR PROJECT DIRECTORY>
                  command: /bin/sh ./deploy.sh

Remember the Github Secrets we set in the repo? This is where we use them. The Secrets we set become the values here for aws-access-key-id, aws-secret-access-key, and instance-ids which allows AWS SSM Send-Command to access our EC2 instance.

There are 3 parts of the .yml file you want to make sure you change for your project:

  1. The aws-region should be the same Region as where you have created your EC2 instance. (If you do not know check the top left of your AWS EC2 Console to confirm the Region you are in).
  2. working-directory should be the working directory where you created the deploy.sh script.
  3. command should be the directions you would like to run. For our case, we created a simple script so that it does not complicate the command line here but you can add as many commands here as long as you are following the .yml syntax.

Once the file is set, go ahead and git add, commit, and push to your repo and the magic should start!


Issues with Github Actions

I struggled with getting this to work and it took several tries. I found that if for whatever reason there are problems with your Github Actions deployment it helps to look through the errors. To find the errors go to your Github project repo:

  1. Go to Actions
  2. You should see a list of workflows or commits you have made since creating the .yml file.
  3. Click the most recent one.
  4. Click on the start link which will show you each step of the job ran.
  5. Click through to the command with a red X and find the errors there.

If you do have issues feel free to reach out to me or peterkimzz by creating an issue here if you feel that you have done everything 100% and it still does not work. peterkimzz was extremely responsive and helpful when I was struggling to get this working (Thank you!).

Back to Table of Contents


Setting up your Domain

This is an extra step if you decided to want to buy a domain and use it for your project (I recommend it!). There is great satisfaction in being able to tell people to go to www.your-awesome-site.com and have people see your hard work!

To get started you would need to first purchase a domain. I most commonly use Google Domains but another popular domain registrar is GoDaddy. Whichever registrar you use, make sure you have purchased the domain that you want!

There are two things we would need to configure to connect our project with our domain:

  • Create records on our domain DNS with our registrar.
  • Configure NGINX on our EC2 instance to recognize our domain.

Creating Domain records

Let's start with configuring our DNS with records:

  • Go to the DNS portion of your registrar.
  • Find where you can create custom resource records.

Set the records like so:

Name Type TTL Data
@ A 1h YOUR-ELASTIC-IP-ADDRESS
www CNAME 1h your-awesome-site.com

Once that is set we are good to move on to configure our Web Server!

Configuring our Web Server

Let's configure our Web Server, in our case NGINX to recognize our domain!

Start by going to your EC2 Instance and going to our NGINX config file:

% sudo vim /etc/nginx/sites-available/default

Update the first section of the config file like so:

server {
  server_name <YOUR-ELASTIC-IP> your-awesome-site.com www.your-awesome-site.com;
  ...

We are simply adding our root domain and our sub domain (In our case with the prefix www) to our NGINX config.

Next as we always should do after changing our NGINX config file run:

sudo sudo systemctl restart nginx

And Voila! You are done! Note: Sometimes the domain change does not happen immediately. From my experience it can happen almost instantaneously to a few hours. If the changes haven't happened after 48 hours, double check your work to see if there are any typos or errors.


HTTPS

SSL or Secure Sockets Layer allows HTTPS requests to happen. Our current project currently uses HTTP requests which can be dangerous for the potential users of your web app. Therefore I always recommend making sure that you are using HTTPS.

For details on why HTTPS over HTTP this article is a pretty good deep dive on why.

Alright! We will be working with Certbot which is provided by letsencrypt.org which is a non-profit organization which helps create SSL Certificates. They are widely used and best of all, FREE!


Installing Certbot

On your browser go to https://certbot.eff.org/instructions.

There select the Software and Operating System (OS) you are using. For our example, we are using NGINX and Ubuntu 18.04 LTS (bionic).

Go to your EC2 Instance and follow the instructions until they ask you to run the command:

% sudo certbot --nginx

After running the command certbot will prompt you with several options, the first being: Which names would you like to activate HTTPS for? And if your NGINX config is configured correctly, should show both your root domain as well as with the www subdomain, like so:

1: your-awesome-site.com
2: www.your-awesome-site.com

I usually recommend just hitting Enter to activate HTTPS for both because, why not?!

The next prompt would be:

Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: No redirect - Make no further changes to the web server configuration.
2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for
new sites, or if you're confident your site works on HTTPS. You can undo this
change by editing your web server's configuration.

I typically go with 2: Redirect as it seems to make more sense to have all requests be in HTTPS. There are probably situations where it is not the best option but for our case we will go with this one.

Afterwards, Certbot will go ahead and make a few changes to our NGINX config file.

Note: Once your site is using Https, make sure to double check your API calls and make sure that they are making calls with https:// rather than http://. This may be an unnecessary precaution but I have had issues with this in the past.

After a few moments checkout your domain at your-awesome-site.com.

Check to make sure that there is a lock icon next to your site.

secure_site

Congratulations! You have successfully deployed a web app with HTTPS!

Back to Table of Contents

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment