Setting up a CI/CD Pipeline for React app using Github Actions (Including creation of infrastructure using AWS CDK)
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
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
When you visit, you could see the Publicly accessible
bucket as shown below
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 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.
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:
- 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 withpull_request
, this workflow will be triggered for all of sub types ofpull_request
. For example, this workflow will be run even when push any changes to pull_request. - We're using
actions/checkout@v3
action to checkout the code. Just to reiterate, github workflow will not checkout your code by default - We're using
npm ci
to install dependencies of react app.ci
represents continuous integration mode. When you install dependencies inci
mode - it will deterministic and it will not be able to install any global packages. It will be faster. - 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. - 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.
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:
- We've changed the trigger to
closed
subtype ofpull_request
as we want this workflow to run when the PR is closed. - 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. - 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. - We need permissions for this token, Hence, we've added necessary permissions to read contents.
- 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