Setting up a CI/CD Pipeline for React app using Github Actions (Including creation of infrastructure using AWS CDK)

Setting up a CI/CD Pipeline for React app using Github Actions (Including creation of infrastructure using AWS CDK)
Photo by Rubaitul Azad / Unsplash

In this guide, you're going to learn how to create an infrastructure in AWS to host react application - in fact, the steps would be same for all frameworks which ultimately generate static websites - such as Angular, Vue etc.. Then, we're going to learn about how to set up CI/CD (Continuous Integration / Continuous Deployment) pipeline to integrate and deploy your changes automatically once you check-in the changes in Github repository.

Infrastructure creation:

AWS S3 service has a capability to host static website. So, the infrastructure is pretty simple - it is just a s3 bucket with simple configuration to make the bucket to behave like a website.

For this, we're going to AWS CDK - an Infrastructure as Code (IaC) tool - developed and maintained by AWS. If you're new to AWS CDK - I strongly suggest you to read this guide to understand AWS CDK. No problem, I can wait.

Having understood the basics of AWS CDK - let us create AWS CDK app using typescript. First, create a new directory. AWS CDK CLI requires to have an empty directory for it to create CDK app. Then, you can execute the command cdk init app to create a new CDK app.

mkdir aws-cdk-static-website
cd aws-cdk-static-website
cdk init app --language=typescript

CDK app would have created an empty stack - with an example resource for the queue. Now, we're going to create a bucket in that stack

 const wesbiteBucket = new s3.Bucket(this, "WebsiteBucket", {
      bucketName: "react-app-static-website-bucket",
      publicReadAccess: true,
      websiteIndexDocument: "index.html",
    });

In the above Bucket construct, we're passing 3 parameters:

bucketName - as the name implies, this would be the name of the bucket created

publicReadAccess - to host a website in S3 bucket - the bucket should've public read access

websiteIndexDocument - When you access the dynamically generated domain - this contents of the file mentioned here would be displayed. As of now, we want index.html to be displayed when users access the website.

Please note that we've not created this index.html till now. Ultimately, this index.html would be generated by building react application.  Our infrastructure setup doesn't care whether it is a react app or angular app. All it needs is a HTML file by name index.html .

So, we're going to create a simple bare-bone html file and  we're going to deploy that HTML file when we deploy this CDK app.

Create a directory by name website at root level of the CDK app and create a simple HTML file by name index.html . As mentioned earlier, the contents of the file doesn't matter.

Then, we're doing to deploy the contents of website directory(just index.html in our case) into the bucket created earlier.

new s3deploy.BucketDeployment(this, "BucketDeployment", {
      sources: [s3deploy.Source.asset("./website/")],
      destinationBucket: wesbiteBucket,
    });

This BucketDeployment construct needs couple of parameters - sources to indicate the contents that have to uploaded into bucket and destinationBucket represents the target bucket (website bucket in our case)

Below is the complete contents of the stack

import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as s3deploy from "aws-cdk-lib/aws-s3-deployment";


export class AwsCdkStaticWebsiteStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const wesbiteBucket = new s3.Bucket(this, "WebsiteBucket", {
      bucketName: "react-app-static-website-bucket",
      publicReadAccess: true,
      websiteIndexDocument: "index.html",
    });

    new s3deploy.BucketDeployment(this, "BucketDeployment", {
      sources: [s3deploy.Source.asset("./website/")],
      destinationBucket: wesbiteBucket,
    });
  }
}

Below is the pictorial representation of our infrastructure created using AWS CDK

AWS CDK static website
AWS CDK static website

I hope you've already bootstrapped CDK app - as explained in this article. If not, please execute below command

cdk bootstrap

Execute the below command to deploy the CDK app

cdk deploy

CDK app will ask for necessary permissions and roles to be created for it to deploy. Once it is deployed, you'll be notified in terminal. Then, you can log-in to the AWS console to see the changes.

Below is the screenshot of CloudFormation stack that has been created by CDK app

Cloudformation stack created by AWS CDK
Cloudformation stack created by AWS CDK

When you visit, you could see the Publicly accessible  bucket as shown below

Website Bucket Created by AWS CDK
Website Bucket Created by AWS CDK

CI/CD pipeline with Github Actions:


We're going to create a simple react app and then we're going to write CI/CD pipeline. If you're not familiar with Github Actions, I strongly recommend you to read this guide on Github Actions.

Execute below command to create react app (I'm using TypeScript). The name of the react app is github-actions-react-app and using npm as package manager. Of course, you can use yarn if you're more familiar with it.

npx create-react-app github-actions-react-app --template typescript --use-npm

When you run the application, you'll see below page.

We're going to follow GitHub Flow workflow. This is a light weight workflow where you create a branch of your default branch( main or master ). You commit the changes for your feature into this feature branch and raise PR once you complete your feature. Then, get your PR reviewed and once it is reviewed - you can merge the changes into your default branch.

Below is the pictorial representation for the same.

Github Flow
Github Flow

Github actions workflows:

We're going to create couple of workflows - one would run when pull_request is created and another the changes are merged into master.

Pull request workflow:

When a pull request is raised, we want to checkout code, install packages, run tests and build react app.

Pull request workflow
Pull request workflow

Below is the workflow created for the same.

name: Build & test react app

on:
  pull_request:

jobs:
  build-test-react-app:
    runs-on: ubuntu-latest
    steps:
      - name: checkout code
        uses: actions/checkout@v3
      - name: install packages
        run: npm ci
      - name: run tests
        run: CI=true npm run test
      - name: build app
        run: npm run build

Some things to note in above workflow:

  1. We have used pull_request as the trigger for this workflow - as we want this workflow to be triggered only when pull_request is created. As we didn't mention any of the subtypes with pull_request , this workflow will be triggered for all of sub types of pull_request. For example, this workflow will be run even when push any changes to pull_request.
  2. We're using actions/checkout@v3 action to checkout the code. Just to reiterate, github workflow will not checkout your code by default
  3. We're using npm ci to install dependencies of react app. ci represents continuous integration mode. When you install dependencies in ci mode - it will deterministic and it will not be able to install any global packages. It will be faster.
  4. While running tests also, we're setting CI flag so that the tests will not be interactive. If there is any issue with tests, it will throw error.
  5. Final step is to build react app - just to make sure that build runs fine (even though we're not going to use build)

When you create PR, this workflow will be executed and you can see that check in Github console as shown below

Deploy workflow:

When PR is reviewed and approved, the changes are merged into master. This react application will be deployed to AWS S3 bucket that we've created earlier during infrastructure setup.

When you notice the difference between pull_request workflow and deploy workflow is just deployment step. All other previous steps (checkout, install, test and build) will happen in this workflow as well.

deploy workflow
deploy workflow

To deploy react app in AWS, github workflows should have permission to deploy in AWS. One of the option is to store AWS access key and AWS secret key is github secrets. But, generally, it is not recommended to store keys in long term.

So, we're going to use Github OIDC connect. The idea is to configure AWS to accept Github as Identity provider and create a role. I've written a  step by step guide on how to configure Github OIDC here. Please read that article.

Once you configured Identity Provider and created role, you can create deploy workflow - as shown below. The changes are highlighted in bold.


name: build & deploy react app

on: 
  pull_request:
    types:
    - closed 

jobs:
  deploy-react-app: 
    if: github.event.pull_request.merged == true 
    runs-on: ubuntu-latest 
    permissions:
      id-token: write
      contents: read 
    steps:
      - name: checkout code
        uses: actions/checkout@v3
      - name: install packages
        run: npm ci
      - name: run tests
        run: CI=true npm run test
      - name: build app
        run: npm run build 
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: arn:aws:iam::your-account-number:role/github-actions
          aws-region: us-east-1
      - name: Deploy react app
      run: aws s3 sync ./build/ s3://react-app-static-website-bucket 

Things to note in this deploy workflow:

  1. We've changed the trigger to closed subtype of pull_request as we want this workflow to run when the PR is closed.
  2. An important point to note here is that PR can be closed even without merging. So, we need to check whether the PR is merged. Hence, we've condition to the job github.event.pull_request.merged to be true.
  3. After building the application - we've configured AWS credentials to use a particular role github-actions so that any subsequent steps to use the token generated.
  4. We need permissions for this token, Hence, we've added necessary permissions to read contents.
  5. At the last step, we're running sync command - to deploy react app build directory to S3 bucket

When you merge the PR, the build & deploy workflow will run and deploy the code. You can see the updated contents of the bucket in AWS S3 console.

And, when you access the S3 public website URL, you can see the react website