How to build, deploy and host static website and node.js applications using CI/CD pipeline at AWS

If you have an idea and you want to make it public as soon as possible or if you just need a quick, resilient and sustainable solution for deploying and hosting your app, this tutorial is for you. I am going to show you how to setup CI/CD pipeline to build, deploy and host static client and node.js server app at AWS. The goal of this article is to have both client and server side code deployed from Github repository to AWS and be able to run build and deployment pipeline automatically after every push to the Github repository.

Create mono-repo for client and server side code

We are going to choose monorepo over two separate repositories in this case because of a few reasons:

  • atomic changes - you don’t have to synchronise deployment of client and server because you are deploying both at the same time
  • ease of code reuse
  • better development experience Here is out “Hello world” app repository. For the client side I am going to use create-react-app and for the server side node.js.

The folder structure of example application is going to look like this:

project folder structure

AWS Regions

To reduce data latency in your applications, most Amazon Web Services offer a regional endpoint. Before creating the new service in AWS console always make sure that you are in the correct region, you can check it on the right side of the top bar. Exception is CloudFront which only has one global region situated in N.Virginia. That’s why we will have to create the SSL certificate for CloudFront in this specific region, for every other service we will choose the same one region (in our example app Frankfurt).

AWS console - region select in top bar

Setup DNS

Amazon Route 53 is a highly available and scalable cloud Domain Name System (DNS) web service. It is designed to give developers and businesses an extremely reliable and cost effective way to route end users to Internet applications by translating names like www.example.com into the numeric IP addresses like 192.0.2.1 that computers use to connect to each other. Amazon Route 53 is fully compliant with IPv6 as well.
Amazon Route 53 overview

In this section, we are going to create Hosted zone in AWS Route 53, point our domain to this newly created hosted zone and create SSL certificates in Certificate Manager.

Create Hosted zone in Route 53

  1. Go to Route 53
  2. Click “Hosted zones” and “Create hosted zone”
  3. In “Domain name” input enter the name of you domain (without subdomain) yourdomain.com AWS Route 53 - create hosted zone
  4. Click “Create”

Change NS records at your domain provider (required only if you didn’t buy a domain at Amazon)

  1. Go back to your Hosted zone in Route 53
  2. Copy “Name servers” values AWS Route 53 - hosted zone's name servers
  3. Go to page where you’ve bought domain and change the NS records to match copied “Name servers” from the previous step

Request SSL certificate in Certificate Manager

  1. Navigate to Certificate Manager
  2. Make sure you’re in N.Virginia region, it’s the only region which is available for CloudFront
  3. Click “Request a certificate”
  4. In “Domain name” input enter yourdomain.com, if you want to setup subdomain enter *.yourdomain.com
  5. Go through the wizard and validate your certificate (I prefer to validate it with DNS, you just need to click the button which adds DNS record to Route 53 automatically)

NOTE: We need to create the same SSL certificate again, but this time, it has to be in the same region as your Elastic Beanstalk instance will be. If your Elastic Beanstalk instance will be in N.Virginia region skip this step.

Create Elastic Beanstalk to host server node.js app

AWS Elastic Beanstalk is an easy-to-use service for deploying and scaling web applications and services. You can simply upload your code and Elastic Beanstalk automatically handles the deployment, from capacity provisioning, load balancing, auto-scaling to application health monitoring.
AWS Elastic Beanstalk – Deploy Web Applications

I’ve chosen Elastic Beanstalk because of simplicity and ease of use. On top of this, it supports auto-scaling and pay-as-you-go.

Create Elastic Beanstalk application

  1. Go to Elastic Beanstalk
  2. Click “Create New Application“
  3. Enter “Application name”
  4. Click “Create”

Create environment

  1. Click “Actions” and then “Create environment”
  2. Enter “Environment name” and “Domain”
  3. For “Platform” select node.js “Preconfigured platform”
  4. For “Application code” select “Sample app” for now AWS Elastic Beanstalk - create a web server environment
  5. Click “Create environment”

For now, there is only a sample AWS application deployed. We are going to deploy ours using CodePipeline in the next steps.

The node.js Elastic Beanstalk platform sets the PORT environment variable to the port which the proxy server passes traffic to. Read this variable in your code to configure your application’s port:

const port = process.env.PORT || 3000

app.listen(port, () => {
  console.log(`Example app listening on port ${port}!`)
})

Point domain to Elastic Beanstalk instance

  1. Go to Route 53 into your hosted zone
  2. Click “Create Record Set”
  3. Inside “Name” enter subdomain for you server app (for example api.yourdomain.com)
  4. Inside “Type” select “CNAME”
  5. Inside “Value” enter your Elastic Beanstalk URL (you can find it in the Elastic Beanstalk dashboard) AWS Route 53 - create record set for your Elastic Beanstalk server

Setup Elastic Beanstalk to use SSL

In order to use an SSL certificate for your Elastic Beanstalk App, you’ll need to change the configuration of your app to use Load Balancersas opposed to a single instance. This can get costly, so make sure to check your billing dashboard to ensure you’re not going over your budget.

  1. Go to Elastic Beanstalk
  2. Click “Configuration”
  3. Click “Modify” under “Capacity” tab
  4. Change “Environment type” to “Load balancer”
  5. Click “Apply” (This might take some time, so you might need to wait before proceeding to next steps)
  6. Click “Modify” under “Load balancer” tab
  7. Uncheck “Enabled” toggle in default setting
  8. Click “Add listener”
  9. Inside “Listener port” enter 443
  10. Inside “Listener protocol“ select HTTPS
  11. Inside “Instance port” enter 80
  12. Inside “Instance protocol“ select HTTP AWS Elastic Beanstalk - add listener for redirecting HTTP to HTTPS
  13. In “SSL certificate” select your certificate created in previous steps
  14. Click “Add” button (modal should be closed after this step)
  15. Click “Apply” button (if you don’t apply your settings, all will be dismissed)

Create S3 bucket and CloudFront distribution to host client

S3 is designed to store and retrieve any number of files or objects from anywhere on the internet. It’s simple to use and offers durable, highly available and scalable data storage at a low cost. CloudFront is a content delivery network (CDN) service that delivers static and dynamic web content, video streams and APIs around the world, securely and at a scale. By design, delivering data from CloudFront can be more cost-effective than delivering it from S3 directly to your users. CloudFront serves content through a worldwide network of data centers called Edge Locations. Using edge servers to cache and serve content improves performance by providing content closer to where viewers are located. We are going to create S3 bucket which will be private, then create CloudFront distribution which will have permissions to read from private S3 bucket therefore users could access files only through the CloudFront. Serving content throughout the CloudFront rather than S3 is faster, more secure and cheaper.

Create S3 bucket

  1. Go to S3
  2. Click “Create bucket“
  3. Inside “Bucket name” enter the name for you bucket, I would recommend to name it according to the domain you want to serve it at.
  4. Select region, but it doesn’t matter which one cause we’ll use CloudFront for the webpage delivery. I would try to keep it in the same region as other services just for the convenience
  5. Click “Create”

Create CDN distribution at CloudFront

  1. Navigate to CloudFront
  2. Click “Create Distribution”
  3. Click “Get Started” under the Web distribution
  4. Inside the “Origin Domain Name” select S3 bucket you’ve created in previous step
  5. Inside “Restrict Bucket Access” select “Yes”
  6. Inside ”Origin Access Identity” select “Create a New Identity”
  7. Inside “Grant Read Permissions on Bucket” select “Yes, Update Bucket Policy”. This step adds Bucket Policy to our S3 bucket to allow CloudFront read the files
  8. Change “Viewer Protocol Policy” to “Redirect HTTP to HTTPS” AWS CloudFront - create distribution 1
  9. Under “Distribution Settings” inside “Alternate Domain Names (CNAMES)” input enter your domain name (yoursubdomain.yourdomain.com)
  10. Select “Custom SSL Certificate” in the “SSL certificate” section
  11. Find and select your SSL certificate you’ve created in previous steps
  12. Inside “Default Root Object” enter root file for your client app (index.html) AWS CloudFront - create distribution 2
  13. Click “Create Distribution”

Turn off caching of client root file index.html

  1. Navigate to CloudFront
  2. Click on distribution you’ve created in previous step
  3. Click “Behaviors” tab
  4. Click “Create Behaviour”
  5. Inside “Path Pattern” enters your client root file name (index.html)
  6. For “Object Caching” select “Customize”
  7. Set “Maximum TTL” and “Default TTL” to 0 AWS CloudFront - create do not cache behavior.png
  8. Click “Yes, edit”

Point domain to CloudFront URL

  1. Go to your Hosted zone in Route 53
  2. Click “Create record set”
  3. Inside “Name” enter your subdomain or leave it blank if you are setting root domain
  4. For “Type” select “CNAME”
  5. Inside “Value” enter your CloudFront URL. You can find at CloudFront dashboard in “Domain Name” column
  6. Click “Create”

Test it

  1. Go to your S3 bucket
  2. Upload your website (it has to have an “index.html”)
  3. Go to your website URL (if you get 403, you have to wait until CloudFront becomes configured - it might take a few minutes)

Setup CodeBuild and CodePipeline to automate build and deployment

AWS CodePipeline is a continuous delivery service you can use to model, visualize, and automate the steps required to release your software. You can quickly model and configure the different stages of a software release process. CodePipeline automates the steps required to release your software changes continuously.
What Is AWS CodePipeline?

Create CodePipeline

  1. Go to CodePipeline
  2. Click “Create pipeline”
  3. In the first step “Choose pipeline settings” enter “Pipeline name”. In “Service role” choose “New service role” and in “Default location” choose “Artifact store” AWS CodePipeline - pipeline settings
  4. In the second step “Add source stage” select Github source
  5. Click on “Connect to Github” and follow the instructions
  6. Back in Create pipeline wizard choose your repository and branch AWS CodePipeline - add souce stage
  7. In the third step “Add build stage” in “Build provider” select AWS CodeBuild
  8. Click “Create new project”, choose your build environment and add environment variables if needed
  9. In the fourth step ”Add deploy stage” choose your S3 bucket from below and check “Extract file before deploy” AWS CodePipeline - add deploy stage
  10. Click “Next”, re-check summary and click “Create pipeline”

Setup output artifacts to supply deploy step

  1. Go to pipeline detail created in previous steps
  2. Click “Edit” button
  3. Click “Edit stage” and then edit icon under the “Build” step
  4. Add client and server into the “Output artifacts” inputs: AWS CodePipeline - add output artifacts into the build step
  5. Click “Save”
  6. Click “Edit stage” and then edit icon under the “Deploy” step
  7. Select client in “Input artifacts” AWS CodePipeline - add input artifacts into the deploy step
  8. Click “Save”
  9. Click “Add action” under the “Deploy” step
  10. Select AWS Elastic Beanstalk in “Action provider”
  11. Select server in “Input artifacts”
  12. In “Application name” select your app. Same goes for the “Environment name”, where you select your environment AWS CodePipeline - add action into the deploy step
  13. Click “Save” and then “Save” again under pipeline detail

Add build config file

Add buildspec.yml to the root folder of your project

version: 0.2

phases:
  pre_build:
    commands:
      - cd client && npm install && cd ..
      - cd server && npm install && cd ..
  build:
    commands:
      - cd client && npm run build && cd ..
  post_build:
    commands:
      - mv ./client/build ./build
artifacts:
  secondary-artifacts:
    client:
      files:
        - '**/*'
      base-directory: build
    server:
      files:
        - '**/*'
      base-directory: server

buildspec.yml file has two sections, phases and artifacts. In phases, you can see the commands for installing the dependencies, building and moving the build output to the root folder. From my own experience, it’s impossible to set an artifact’s base-directory to a nested folder and that’s why we move it to the root folder. It’s important to notice that the keys in secondary-artifacts have to match “Output artifacts” in the “Build” stage of CodePipeline.

Summary

Now you are ready to build something great and you don’t have to worry about deploying your code by hand. Every commit to your target branch will run the CodePipeline which builds and deploys your client and server code. Another advantage of this setup is that all the technologies used here are managed, support auto-scaling and have pay as you go model which could be a good foundation for almost every web app.

Resources:

Sudo Labs s.r.o.Vcelarska Paseka 1991/1 040 01 Kosice
ID50415719
VAT IDSK2120313712
VAT2120313712