AWS CI/CD for React Apps Using GitHub Actions, S3, CloudFront, and Route53

Deep hands-on understanding on React deployment steps using AWS resources

Haddad
9 min readJun 1, 2023

Introduction

The modern web development landscape is changing at a rapid pace, and developers are leveraging numerous technologies to improve their workflow and productivity. One such practice is Continuous Integration and Continuous Deployment (CI/CD). In conjunction with cloud storage solutions and Content Delivery Networks (CDNs), CI/CD practices can significantly enhance the development process.

In this article, we’ll explore the CI/CD pipeline for a React application, deploying the application on Amazon S3, and delivering it via Amazon CloudFront using GitHub Actions.

The Role of GitHub Actions

GitHub Actions provides developers with powerful CI/CD capabilities directly within GitHub. It allows automatic execution of software pipelines whenever there’s a trigger like a push or a pull request to specific branches. For a React application, this could mean automatically installing dependencies, running tests, building the application, and deploying it to the desired hosting platform.

The Workflow YAML File

A GitHub Actions workflow is defined in a YAML file. The file contains the steps required to build, test, and deploy your application. For our React app, the file could look something like this:

name: Node CI-CD for build, test, and deploy
on:
push:
branches:
- main
- production
- PBI-**
- coldfix
pull_request:
branches:
- production
jobs:
install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use node 16.x
uses: actions/setup-node@v2
with:
node-version: 16.15.1
- run: npm ci
build:
needs: install
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [ 16.x]
steps:
- uses: actions/checkout@v3
- name: Use node ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run build

sonarscan:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: run test and coverage
run: |
npm install
npm test
- name: SonarQube Scan
uses: sonarsource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/production' || github.ref == 'refs/heads/coldfix'
steps:
- uses: actions/checkout@v3
with:
python-version: 3.9
- name: Cofig AWS Cred
run: |
pip install awscli
aws configure set region ap-southeast-2
aws configure set output json
aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY }}
aws configure set aws_secret_access_key ${{ secrets.AWS_SECRET_KEY }}
- name: deploy with s3 sync and Invalidate CloudFront cache
run: |
npm install
if [ ${{ github.ref }} = 'refs/heads/main' ]
then
export REACT_APP_BACKEND_URL=${{ secrets.REACT_APP_BACKEND_URL_STAGING }}
npm run build
aws s3 sync ./build s3://${{ secrets.S3_BUCKET_NAME_STAGING }}/
aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID_STAGING }} --paths "/*"
elif [ ${{ github.ref }} = 'refs/heads/production' ]
then
export REACT_APP_BACKEND_URL=${{ secrets.REACT_APP_BACKEND_URL_PRODUCTION }}
npm run build
aws s3 sync ./build s3://${{ secrets.S3_BUCKET_NAME_PROD }}/
aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID_PROD }} --paths "/*"
fi

This YAML file defines a workflow that installs the required Node.js version, installs project dependencies, builds the React app, and then deploys the application to Amazon S3. After that, it invalidates the CloudFront cache to ensure that the latest version of the app is served to users.

Understanding {Secrets} Environment Variable

Secrets are encrypted environment variables that you create in an environment, organization, repository, or repository environment in GitHub. They can be used to manage sensitive data, such as access tokens, passwords, or environment variables, that your project needs to function.

Secrets are used in GitHub Actions workflows to provide these sensitive details without exposing them in your code or log files. In a YAML file, secrets are accessed via the secrets context. For example, secrets.AWS_ACCESS_KEY would access the secret named AWS_ACCESS_KEY.

In your CI/CD workflow, secrets are used to store sensitive information needed for deploying your React application to AWS. Specifically:

  • AWS_ACCESS_KEY and AWS_SECRET_KEY are your AWS credentials. They are necessary to authenticate with AWS and perform operations, such as syncing your build files to S3 or invalidating a CloudFront cache.
  • S3_BUCKET_NAME_STAGING and S3_BUCKET_NAME_PROD are the names of your S3 buckets for the staging and production environments, respectively. These are the destinations for your built React application.
  • CLOUDFRONT_DISTRIBUTION_ID_STAGING and CLOUDFRONT_DISTRIBUTION_ID_PROD are the IDs of your CloudFront distributions for the staging and production environments, respectively. They are needed to invalidate the CloudFront cache for the corresponding environment after deployment.
  • REACT_APP_BACKEND_URL_STAGING and REACT_APP_BACKEND_URL_PRODUCTION are likely the URLs of your backend API for the staging and production environments, respectively. These are set as environment variables during the build process, and they are probably used by your React application to make API calls.

It’s essential to use secrets to store this kind of sensitive information. Exposing such details publicly could lead to unauthorized access to your AWS resources, and potential misuse. By using secrets, these details are encrypted and kept safe, and they can only be accessed by authorized GitHub Actions workflows.

Staging-Production Separation

In our GitHub Actions workflow, the staging and production environments are defined by specific branches in your Git repository: main represents the staging environment, while production represents the production environment. These branches are typically used to manage different versions of your application during the development process.

When a push is made to the main branch, it's assumed that this represents a change meant for the staging environment. The staging environment is usually a replica of the production environment, used for testing and verifying changes before they are pushed to production.

Likewise, when a push is made to the production branch, this triggers a deployment to the production environment. The production environment is where your live application resides. Changes here will be visible to your end-users.

In our YAML workflow:

  • When a push is made to the main branch, the environment variable REACT_APP_BACKEND_URL is set to secrets.REACT_APP_BACKEND_URL_STAGING. This probably changes the API endpoint that your React application uses during the build process. The built application is then synced to the staging S3 bucket (secrets.S3_BUCKET_NAME_STAGING), and the CloudFront cache for the staging environment is invalidated.
  • If a push is made to the production branch, REACT_APP_BACKEND_URL is set to secrets.REACT_APP_BACKEND_URL_PRODUCTION. The application is built, synced to the production S3 bucket (secrets.S3_BUCKET_NAME_PROD), and the CloudFront cache for the production environment is invalidated.

This process allows for different versions of your React application to exist simultaneously in different environments, facilitating testing and deployment.

AWS IAM Access Key for Simplicity

AWS IAM, or Identity and Access Management, is a key service within AWS that allows you to manage access to your AWS services and resources securely. Using IAM, you can create and manage AWS users and groups and use permissions to allow and deny their access to AWS resources.

When it comes to using AWS in your CI/CD pipeline, like in this GitHub Actions workflow, you’re interacting with various AWS services such as S3 for storage and CloudFront for content delivery. To do this securely, you need a way to authenticate and authorize these interactions. This is where IAM comes in, particularly with the concept of IAM Users and Access Keys. Each IAM user can have access keys associated with it — an access key ID and a secret access key. These are used to perform programmatic calls to AWS, which is exactly what you’re doing in your GitHub Actions workflow. When setting up AWS CLI in the CI/CD pipeline, the aws_access_key_id and aws_secret_access_key are set using these access keys.

In the context of our GitHub Actions workflow, we’re using secrets (secrets.AWS_ACCESS_KEY and secrets.AWS_SECRET_KEY) to store the IAM user's access key ID and secret access key. This is excellent because it's crucial to keep these credentials secure - they should never be hardcoded into your codebase, as they can provide access to your AWS resources.

Create an Access Key on IAM User Section

When the CI/CD pipeline runs, it retrieves these credentials from the secrets and uses them to set up the AWS CLI configuration. This enables the pipeline to interact securely with your AWS services.

Updating The Key to the repository secrets and variables so it can be accessed on the YAML

Preparing Deployment to Amazon S3 and CloudFront

Amazon S3 (Simple Storage Service) is a storage service that allows you to store and retrieve any amount of data at any time from anywhere on the web. It is ideal for hosting static websites or web applications like a React app.

The built React application is a collection of static files, which are uploaded to an S3 bucket. The bucket is configured to behave as a web host, serving the application’s static files. Create an S3

Create an S3 Bucket AWS

Amazon CloudFront is a CDN service that securely delivers data, videos, applications, and APIs to customers globally with low latency and high transfer speeds. After deploying the application to an S3 bucket, a CloudFront distribution is used to deliver the application to end-users. CloudFront ensures that the application loads quickly, no matter where users are located globally.

Create CloudFront pointing to our S3 Bucket

When the application is updated, the CloudFront cache needs to be invalidated to ensure that users receive the latest version of the app. Invalidation removes the files from CloudFront edge locations, and the next time a viewer requests the invalidated files, CloudFront returns to the origin (the S3 bucket in this case) to fetch the latest version of the invalidated files.

AWS Route53 for cleaning the mess

Amazon Route 53 is a scalable and highly available Domain Name System (DNS) web service offered by AWS. It is designed to give developers and businesses an extremely reliable and cost-effective way to route end users to Internet applications by translating human-readable domain names (like www.example.com) into the numeric IP addresses (like 192.0.2.1) that computers use to connect to each other.

Here’s a deeper look at its functionalities:

Domain Registration: Route 53 lets you register domain names under a variety of top-level domains (like .com, .net, .org) and ensures the domain’s DNS settings are correctly configured.

DNS Routing: After a domain is registered, Route 53 helps route traffic to your application or website by translating friendly domain names to numerical IP addresses, facilitating connections between users and your application.

Health Checking: It can also perform health checks to monitor the health and performance of your application and its endpoints. If an outage should occur, Route 53 can automatically route traffic to healthy endpoints or to resources in a different location.

Integration with AWS Services: Route 53 is integrated with other AWS services such as AWS CloudFront, S3, and EC2, making it easy to route global traffic to your AWS resources.

In the context of your GitHub Actions workflow for a React application, after your application is built and deployed to an S3 bucket, and the CloudFront distribution cache is invalidated, the final step would be to ensure that your Route 53 records are correctly pointing to your CloudFront distribution.

Now lets create a hosted zone:

kapal.tech as a hosted zone on AWS rout3 53

Next step, create an a record A on the hosted zone pointing to our resources:

Create a record for staging

Value’ on the right side is the url of our CloudFront CDN that point to the S3 Bucket.

This way, when a user enters our ‘vessel-staging.kapal.tech’ domain into their web browser, Route 53 correctly routes the request to the CloudFront distribution, which fetches the latest version of your application from the S3 bucket, ensuring that your users always have access to the most recent version of your application.

Conclusion

This is the final look of the architecture that we’ve built so far:

Infrastructure Design for React using AWS resources

In conclusion, automating the CI/CD pipeline using GitHub Actions for a React application and deploying it with AWS services, including S3, CloudFront, and Route 53, can greatly enhance the development and deployment process. This workflow not only ensures a consistent and reliable build process but also delivers the application with high availability and performance.

By segregating staging and production environments through different branches and buckets, we can efficiently manage and test our application before it reaches the end-users. The use of AWS IAM for secure AWS access and environment secrets for secure storage of sensitive information fortify the security of our pipeline.

This setup not only brings robustness to our React application’s deployment but also frees up developers’ time by reducing manual deployment efforts and focusing more on feature development and enhancements.

Remember, automating processes is a key aspect of contemporary web development, and tools like GitHub Actions and AWS Services make it more achievable than ever.

--

--

Haddad
Haddad

Written by Haddad

Computer Science Graduate from Univertas Indonesia. Exploring Backend Automation, AI/ML Engineering, and Sofware Infrastructure. Github: @haddad9

No responses yet