Private Host React App in AWS

Have you ever wondered how to host your developer and staging branch, and make it accessible only to developers? I recently took up this challenge to privately host the React application in AWS. In this post, I’ll take you through the steps I took.

Overview

This article covers:

Design Architecture

In this article, we will be deploying the React App in AWS Fargate accessible through Application Load Balancer. The below diagram shows high-level architecture.

Install Dependencies

  1. AWS CLI – Download here
  2. Node.js – Download here
  3. Docker – Download here

Link to Git Repository

Available here.

Create React App

To create a React project run:

npx create-react-app private-hosting && cd private-hosting

Then, open the directory in Code Editor (eg: VS Code).

Dockerfile & Nginx Config

What is Docker?

Docker is a containerisation tool which enables developers to package and run applications in containers. Containers are standardised executable components that combine application source code with the operating system libraries and dependencies required to run the code in an environment. Containers make it easier to deliver distributed applications, and as businesses move towards cloud-native development and hybrid multi-cloud environments, they are gaining popularity.

What is Nginx?

NGINX is an open source web server that can be used for reverse proxying, caching, load balancing, media streaming, and more. Since starting out as a web server designed for maximum performance and stability, NGINX can also function as a proxy server for email, and a reverse proxy and load balancer for HTTP, TCP, and UDP servers, in addition to HTTP server capabilities.

At the root of the project, create a folder named nginx. Create an nginx.conf file under the nginx folder.

server {

  listen 80;

  location / {
    root   /usr/share/nginx/html;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
  }

  error_page   500 502 503 504  /50x.html;

  location = /50x.html {
    root   /usr/share/nginx/html;
  }

}

At the root of the project, create a dockerfile and .dockerignore

Then, copy the below code and paste it in dockerfile.

# Build Environment
FROM node:14.2.0 as build

# Working Directory
WORKDIR /app

#Copying node package files to working directory
COPY package.* .

#Installing app dependency
RUN npm install –silent

#Copy all the code
COPY . .

# RUN Production build
RUN npm run build

# production environment
FROM nginx:stable-alpine

# Deploy the built application to Nginx server
COPY –from=build /app/build /usr/share/nginx/html

# Remove the default Nginx configuration in Nginx Container
RUN rm /etc/nginx/conf.d/default.conf

COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf

# Expose the Application on PORT 80
EXPOSE 80

# Start Nginx server
CMD [“/usr/sbin/nginx”, “-g”, “daemon off;”]

build
node_modules

Create necessary AWS services (using CFN)

What is AWS CloudFormation?

Create ECR

AWSTemplateFormatVersion: 20100909
Description: React App private hosting

Parameters:
  EcrName:
    Type: String

Resources:
  Ecr:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: !Sub ${EcrName}
      ImageScanningConfiguration:
        ScanOnPush: true

Build the infrastructure

At the root of the project, create template.yaml. Copy the below code and paste it into template.yaml.

AWSTemplateFormatVersion: 2010-09-09
Description: React App private hosting

Parameters: 
  PrefixName:
    Type: String

  DockerRegistoryHost:
    Type: String

  DockerImage:
    Type: String
   
Resources:
  PrivateVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: “10.0.0.0/16”
      EnableDnsSupport: ‘true’
      EnableDnsHostnames: ‘true’
      Tags:
        – Key: Name
          Value: !Sub ${PrefixName}-Vpc
 
  PrivateSubnetAz1:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: “10.0.1.0/24”
      VpcId: !Ref PrivateVpc
      # TODO: Change AZ based on your zone
      AvailabilityZone: ap-southeast-2a
      Tags:
        – Key: Name
          Value: !Sub ${PrefixName}-SubnetAz1
 
  PrivateSubnetAz2:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: “10.0.2.0/24”
      VpcId: !Ref PrivateVpc
      # TODO: Change AZ based on your zone
      AvailabilityZone: ap-southeast-2b
      Tags:
        – Key: Name
          Value: !Sub ${PrefixName}-SubnetAz2

  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref PrivateVpc
      Tags:
        – Key: Name
          Value: !Sub ${PrefixName}-RouteTable

  SubnetRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref RouteTable
      SubnetId: !Ref PrivateSubnetAz1

  SubnetRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref RouteTable
      SubnetId: !Ref PrivateSubnetAz2

  ecsTaskRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${PrefixName}-TaskDefinitionRole
      AssumeRolePolicyDocument:
        Version: “2012-10-17”
        Statement:
          – Effect: Allow
            Principal:
              Service:
                – ecs-tasks.amazonaws.com
            Action:
              – ‘sts:AssumeRole’
      Policies:
        – PolicyName: ecsTaskPolicy
          PolicyDocument:
            Version: “2012-10-17”
            Statement:
              – Effect: Allow
                Action:
                  – “ecr:GetAuthorizationToken”
                  – “ecr:BatchCheckLayerAvailability”
                  – “ecr:GetDownloadUrlForLayer”
                  – “ecr:BatchGetImage”
                  – “logs:CreateLogStream”
                  – “logs:PutLogEvents”
                Resource: ‘*’
 
  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow HTTP request
      GroupName: !Sub ${PrefixName}-SecurityGroup
      SecurityGroupIngress:
        – IpProtocol: tcp
          CidrIp: “0.0.0.0/0”
          FromPort: 80
          ToPort: 80
      VpcId: !Ref PrivateVpc

  Cluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Sub ${PrefixName}-Cluster
      ClusterSettings:
        – Name: containerInsights
          Value: enabled
 
  ClusterService:
    Type: AWS::ECS::Service
    DependsOn: Listener80
    Properties:
      LaunchType: FARGATE
      ServiceName: !Sub ${PrefixName}-ClusterService
      Cluster: !GetAtt Cluster.Arn
      TaskDefinition: !Ref ClusterTaskDefinition
      DesiredCount: 1
      NetworkConfiguration:
        AwsvpcConfiguration:
          SecurityGroups:
            – !GetAtt SecurityGroup.GroupId
          Subnets:
            – !Ref PrivateSubnetAz1
            – !Ref PrivateSubnetAz2
      LoadBalancers:
        – TargetGroupArn: !Ref EcsTargetGroup
          ContainerPort: 80
          ContainerName: !Sub ${PrefixName}-Container
 
  ClusterTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Memory: 2048
      Cpu: 1024
      ContainerDefinitions:
        – Name: !Sub ${PrefixName}-Container
          Image: !Sub ${DockerRegistoryHost}/${DockerImage}
          PortMappings:
          – ContainerPort: 80
      ExecutionRoleArn: !GetAtt ecsTaskRole.Arn
      RequiresCompatibilities:
        – FARGATE
      NetworkMode: awsvpc
 
  LoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Sub ${PrefixName}-LoadBalancer
      SecurityGroups:
        – !GetAtt SecurityGroup.GroupId
      Subnets:
        – !Ref PrivateSubnetAz1
        – !Ref PrivateSubnetAz2
      Scheme: internal
 
  Listener80:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref LoadBalancer
      Protocol: HTTP
      Port: 80
      DefaultActions:
        – Type: forward
          TargetGroupArn: !Ref EcsTargetGroup

  EcsTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Sub ${PrefixName}-tg
      TargetType: ip
      HealthCheckEnabled: true
      VpcId: !Ref PrivateVpc
      IpAddressType: ipv4
      Port: 80
      Protocol: HTTP
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: /
      HealthyThresholdCount: 2
      TargetGroupAttributes:
        – Key: deregistration_delay.timeout_seconds
          Value: ’30’

Login to AWS CLI

Follow this documentation to login to AWS CLI.

Create a Makefile

At the project root, create a Makefile. Copy the below code and paste it into Makefile.

 

# TODO: Update the AwsAccountID
AwsAccountId?=
AwsDefaultRegion?=ap-southeast-2
AppName?=PrivateHosting
ImageRepoName?=ecr-react-app
ImageTag?=latest
DockerRegistoryHost=${AwsAccountId}.dkr.ecr.${AwsDefaultRegion}.amazonaws.com

deploy-ecr:
aws cloudformation deploy \
–stack-name ecr-stack \
–template-file ./ecr-template.yaml \
–capabilities CAPABILITY_NAMED_IAM \
–parameter-overrides \
EcrName=${ImageRepoName}

docker-login:
aws ecr get-login-password –region ${AwsDefaultRegion} | docker login –username AWS –password-stdin ${DockerRegistoryHost}

docker-build:
docker build -t ${ImageRepoName}:${ImageTag} .
docker tag ${ImageRepoName}:${ImageTag} ${DockerRegistoryHost}/${ImageRepoName}:${ImageTag}

docker-push:
docker push ${AwsAccountId}.dkr.ecr.${AwsDefaultRegion}.amazonaws.com/${ImageRepoName}:${ImageTag}

deploy-private-hosting:
aws cloudformation deploy \
–stack-name ${AppName} \
–template-file ./template.yaml \
–capabilities CAPABILITY_NAMED_IAM \
–parameter-overrides \
PrefixName=${AppName} \
DockerRegistoryHost=${DockerRegistoryHost} \
DockerImage=${ImageRepoName}:${ImageTag}

deploy: deploy-ecr docker-login docker-build docker-push deploy-private-hosting

Enjoyed this blog?

Share it with your network!

Move faster with confidence