Deploy a React/Node App Using TravisCI and AWS
Let’s say you have a web app project built with React on the front-end and Node on the back-end API. You have the front- and back-end split up into separate GitHub repositories and you fire both of them up on different ports and have the front-end communicate with the API server.
Now let’s say you want to deploy your beautiful web app to a public domain, mywebapp.com
, so the rest of the world can see your work. There are a handful of ways to do this but in this guide, we’re going to focus on just one: continuous integration/deployment with TravisCI and AWS (S3 + CloudFront + ElasticBeanstalk).
This setup is pretty common for small- to medium-scale web applications and it’s what the product I work on, BruinMeet, employs. In addition to a public-facing domain, BruinMeet also has a private “staging” domain to which we deploy to in order to test before releasing changes to the public.
This tutorial will walk you through the steps of setting up all of this.
Why use this approach?
- Continuous integration allows us to make testing an automatic part of our development process. Every time we push up a commit, TravisCI will run our branch’s tests.
- Continuous deployment lets us make pushing to GitHub and deploying the app (almost) synonymous. Unless tests fail, we can push a commit to our
production
branch and soon after have our production site reflect those changes — automatically. - AWS’s ElasticBeanstalk — along with Docker — makes it easy to deploy our Node API server with just a little bit of configuration.
- AWS’s S3 + CloudFront lets us deploy and serve up our front-end super easily, while taking advantage of AWS’s global scale content delivery network (CDN).
Alright, I’m convinced but this sounds complicated. Where do I even start? 😣
First off, let’s establish the prerequisites before diving into this tutorial. You should:
- have a working knowledge of git and GitHub
- have an AWS account and an AWS IAM user configured with the programmatic access and the permissions:
AmazonS3FullAccess
,AWSElasticBeanstalkFullAccess
,CloudFrontFullAccess
,IAMFullAccess
(how to) - have the AWS CLI installed and configured with the above IAM user on your machine
- have both your GitHub repositories hooked up to TravisCI
I’ve dropped links above for the official AWS documentation on how to get set up. Everything else, we’ll figure out as we go through the tutorial. Alright, with that out of the way, let’s get started.
Let’s start by looking at how the deployment architecture will look when we’re done. Below is a diagram that shows the paths that a request coming from a browser (or other client) might take.
Say we send a request to www.mywebapp.com
. This is gonna send a request for the root (/
) resource. That request is going to first reach a CloudFront server (called an edge cache). The CloudFront server will then see if it already has the requested content. If it does, it’ll respond with it (which will be the index.html
in our case) . If not, it’ll request it from the S3 bucket, cache it, then respond to the user with it. We can then repeat this reasoning for any other resources we bundle with our React app (your main bundle.js
, image assets, CSS files, etc.)
What if we instead send a request to api.mywebapp.com
(like our React app might)? This request is going to first meet an ElasticBeanstalk load balancer, which forwards requests to one of the underlying EC2 containers. These underlying EC2 containers are what actually contain the code for our Node server. ElasticBeanstalk’s job is simply to do this forwarding and to (elastically) add or remove containers as our application’s needs change.
Important to note here is that our API is hosted at the
api
subdomain rather than a/api
route. There are certainly ways to achieve the latter but they will require more work and are outside of the scope of this tutorial.
With the end goal in mind, let’s get to work on making the magic happen. I’m gonna split the rest of this tutorial into 4 steps.
- Configure the front-end app for continuous deployment to S3 using TravisCI.
- Configure the back-end app for continuous deployment to ElasticBeanstalk using TravisCI and Docker.
- Configure our domain’s DNS settings to point correctly to both the front-end and back-end apps.
- Configure AWS Certificate Manager to enable HTTPS for our domain.
Step 1: Configure the front-end app
First off, another diagram to keep in mind as we walk through this step.
Most of this UML sequence diagram should be fairly intuitive. If you’re entirely unfamiliar with UML sequence diagrams, the TL;DR on the link above is that the vertical axis represents time, each column is a different entity, and the arrows represent communication between entities. The “alt” box on the bottom simply represents a conditional flow, in which we only deploy to S3 if we determine that we want to for this branch.
Let’s start by setting up our S3 buckets. Open up a terminal session and type the following command to create a new S3 bucket. Change the bucket name and region values to your preference.
Keep in mind that S3 bucket names must be globally unique.
aws s3api create-bucket \
--bucket mywebapp-react-production \
--region us-west-2 \
--create-bucket-configuration LocationConstraint=us-west2
Running this command will output the URL your S3 bucket will be accessible at. We won’t need it for anything in particular so you can ignore it. If you ever need to see it again, you can find it by going to your bucket, then Properties > Static Website Hosting.
With the bucket successfully created, let’s configure it to host a static website. Fortunately, the AWS CLI makes this super easy with the following command. This command’s main job is to make the S3 aware that a request to /
is the same as a request to index.html
.
aws s3 website s3://mywebapp-react-production/ \
--index-document index.html
Next, make the bucket content publicly accessible by going to the S3 bucket in the AWS console and adding the following configuration under Permissions > Bucket Policy, making sure to change the bucket name under “Resource” to match your bucket’s name.
{
"Version":"2012-10-17",
"Statement":[{
"Sid":"PublicReadGetObject",
"Effect":"Allow",
"Principal": "*",
"Action":["s3:GetObject"],
"Resource":["arn:aws:s3:::mywebapp-react-production/*"]
}]
}
Once that’s done, our bucket is set up and ready to host a static website! ✔️
If you’ve been paying close attention, you’re probably recalling the first diagram and thinking, “Okay great, S3 is set up but what about CloudFront?”
Setting up CloudFront is a breeze as well. There are two key steps: (1) Create the distribution with the AWS CLI, (2) Configure the error page to redirect to /
.
Start by running the following command to create a distribution.
aws cloudfront create-distribution \
--origin-domain-name mywebapp-react-production.s3.amazonaws.com \
--default-root-object index.html
This will spit out a huge configuration JSON describing your new CloudFront distribution. The key value to note is Distribution.DomainName
. For me, this was d3ois221lkhb7q.cloudfront.net
. This is the URL that we’ll point our main domain www.mywebapp.com
at later in the tutorial.
Next, we’ll force CloudFront to route 404 errors to /
since our React app will be responsible for displaying a 404 page on its own (via react-router). To accomplish this, open up CloudFront in the AWS Console and in the distribution we just made, go to the Error Pages tab, and “Create Custom Error Response” with the following config.
CloudFront setup. ✔️
Lastly, let’s add a few new files to our project that will allow us to upload our React app bundle to our S3 bucket whenever we push a commit to a certain branch.
As you might notice, there are a couple of variables in here that seem to be coming from nowhere (CLOUDFRONT_DIST_ID_STAGING
and CLOUDFRONT_DIST_ID_PRODUCTION
). We’ll actually need to define them in the TravisCI settings for this repo under “More Options” > Settings > Environment Variables.
With our S3 bucket made, CloudFront distribution set up, these new files in our project, and TravisCI env variables set up, our front-end is good to go. Pushing to master should now cause our React app to deploy to the S3 bucket, which is served up by the CloudFront distribution. As such, it should be accessible at both our S3 bucket endpoint and CloudFront domain name.
A few additional points to consider:
- CloudFront — being a cache — will not automatically detect new versions of your app in S3. As such, if we want changes to be deployed instantly, we’ll need to create an invalidation after deploying to S3. We can either do this in the CloudFront console or programmatically as part of our deployment in
deploy.sh
. - As of now, our CloudFront distribution will only accept requests via the HTTP protocol. HTTPS requests simply won’t work. Step 3 of this tutorial will walk through setting that up using AWS Certificate Manager.
- A great optimization to add to our CloudFront distribution is to enable “Compress objects automatically” under Behavior > Default(*) path pattern > Edit. Turning this lets CloudFront use gzip to compress our files for faster transfers to clients.
Step 2: Configure the back-end deployment process.
The overall process for back-end deployment will be nearly identical to that of the front-end. The only difference is that instead of deploying to S3, we deploy to ElasticBeanstalk. This comes with a few additional tasks but we’ll walk through them step-by-step.
Let’s first set up our ElasticBeanstalk environment on AWS.
We’ll start by creating an ElasticBeanstalk application.
aws elasticbeanstalk create-application --application-name mywebapp-api
We’ll then create the environment.
aws elasticbeanstalk create-environment \
--application-name mywebapp-api \
--environment-name production \
--solution-stack-name "64bit Amazon Linux 2018.03 v2.12.0 running Docker 18.03.1-ce"
The relationship between an EB application and environment is one-to-many: in other words, one application may have one or more environments. For example, if you want to host both a production and staging version of your application, you’d create an environment for each.
Lastly, if we need to define environment variables for our EB application, we can do so by navigating to Configuration > Software (Modify) > Environment properties in the AWS EB Console for the environment in question.
ElasticBeanstalk setup. ✔️
Now, for the new files.
First, we’ll need to add some ElasticBeanstalk configuration details to our project. In the root directory of your project, create a new folder named .elasticbeanstalk
and in it create a file called config.yml
with the following contents:
Now let’s add the files telling TravisCI to push your code to ElasticBeanstalk.
The key difference in this deploy script is the configuration of your AWS credentials before deploying since the eb
command doesn’t pull it from your environment variables like aws s3
does.
Okay, now for a new file exclusive to the back-end deployment process, Dockerfile
.
This file defines the steps that a Docker container needs to run to start up the API server. There’s tons more to learn about Docker here if you’re interested but for the sake of this tutorial, we’ll just consider it as a necessary step in ElasticBeanstalk deployment.
That’s it! We’re done. Now pushing your code to the hooked up branch should automatically kick off a deployment!
Step 3: Configure the domain’s DNS settings
At this point, your front-end and back-end apps should be deployed and independently functional. The problem is, they aren’t hosted on the domain you want (and therefore can’t even communicate with each other)!
The React app is hosted at a CloudFront endpoint that looks something liked3ois221lkhb7q.cloudfront.net
and the API at an ElasticBeanstalk endpoint that looks like production.nghvu3hx5z.us-west-2.elasticbeanstalk.com
.
To point your domain name at these apps, go to your domain hosting service’s DNS settings and add the following new DNS records, tweaking the URLs you put for value
to match your own app’s CloudFront, EB, and domain URL’s. The interface for adding these records varies by hosting service but the general information you need to enter is the same.
# For the React app
Record Type: CNAME
Host: www
Value: d3ois221lkhb7q.cloudfront.net# For the API server
Record Type: CNAME
Host: api
Value: production.nghvu3hx5z.us-west-2.elasticbeanstalk.com# For redirecting mywebapp.com to www.mywebapp.com
Record Type: URL Redirect
Host: @ # This means the root
Value: http://www.mywebapp.com
Give the DNS record changes a generous amount of time to propagate through the network (we’re talking on the scale of hours here).
Step 4: Configure HTTPS using AWS Certificate Manager
At this point, your app should be fully wired up to be accessible via your chosen domain (in other words, navigating to mywebapp.com should work as expected).
But there’s one small problem. Your site isn’t secure.
In order to ensure that communication between your site and its users is secure, you’ll need to add HTTPS/SSL support. Fortunately, AWS makes this a breeze.
Head on over to the Certificate Manager in the AWS Console. Click “Request a Certificate” and then “Request a public certificate”.
Under the domain names section, specify both the root and the www
subdomain.
Click Next and follow the instructions to validate your certificate.
Once that’s done, head on over to your CloudFront instance you created earlier. Under the General tab, click Edit. Under the SSL Certificate section, check “Custom SSL Certificate” and select the certificate you just made. Then under the Behaviors tab, select the first Behavior and Edit, selecting “Redirect HTTP to HTTPS”.
Our React app server is now HTTPS secure. Now let’s secure our API.
Go to the ElasticBeanstalk console and open up your application environment’s configuration panel. In there, click “Modify” under “Load Balancer”.
Finally, click “Add Listener” and create a Load Balancer listener with the following settings, selecting the certificate you just made for the “SSL Certificate” dropdown.
Add the certificate and now your API is secure as well.
Done.
Whew, that was pretty long but you made it to the end. Congratulations! You now have a web app that’s configured to run tests (and conditionally) deploy to AWS S3/CloudFront and ElasticBeanstalk whenever you push to GitHub. 🎉👏😄