DEV Community

Cover image for CloudFormation Template to Create a CI/CD Pipeline for a React application hosted in an S3 Bucket
Gilbert Young Jr
Gilbert Young Jr

Posted on • Edited on

CloudFormation Template to Create a CI/CD Pipeline for a React application hosted in an S3 Bucket

In this article, I will give a step-by step guide on how to create a CI/CD Pipeline for a React Web App hosted as a static website in a S3 Bucket.

Below is an architecture diagram illustrating the AWS services that were used to build this solution.

Image description

Now that you have reviewed the architecture, let's get straight into it.

Create React App

If you want to follow along with your own react application please feel free to do so. If not, head over to GitHub and clone the repo.

Creating buildspec file

Once you have the repo cloned, take a minute to review the buildspec.yml file, this file is used to provide AWS CodeBuild instructions on how to build our application. It has 4 stages, install, pre_build, build and post_build and lastly it output our artifacts files.
Note: The buildspec file is created at the root level inside your directory.

version: 0.2

phases:
  install:
    runtime-versions:
      nodejs: 16

  pre_build:
    commands:
      # install dependencies
      - echo installing dependencies...
      - yarn install

  build:
    commands:
      # run build script
      - echo Build started on `date`
      - echo Building React Application...
      - yarn build

  post_build:
    commands:
      - echo Build completed on `date`

artifacts:
  # include all files required to run application
  # we include only the static build files
  files:
    - "**/*"
   # reference directory where build file is located
  base-directory: "build"

Enter fullscreen mode Exit fullscreen mode

Creating CloudFormation Template

Next, we'll look at our CloudFormation template that creates our resources for the pipeline.

First, we define our parameters. These will later be used as references in our code.

AWSTemplateFormatVersion: 2010-09-09
Description: This template is used to create a CI/CD Pipeline that deploys a React Web app to S3 for static website hosting.

# Parameters to be used through out the template
Parameters:
  Stage:
    Type: String
    Default: dev
  AppName: 
    Type: String
    Default: <APP NAME>
  GithubUserName:
    Type: String
    Default: <GITHUB USERNAME>
  GithubRepo:
    Type: String
    Default: <GITHUB REPO>
  GithubBranch:
    Type: String
    Default: <GITHUB BRANCH>
  GithubOAuthToken:
    Type: String
    Default: <GITHUB ACCESS TOKEN>
Enter fullscreen mode Exit fullscreen mode

Next, we create IAM policies to give our resources adequate permissions.

# Create role for CodeBuild
  CodeBuildRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - 
            Effect: Allow
            Principal:
              Service:
                - "codebuild.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Path: /service-role/
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: "2012-10-17"
            Statement: 
              - 
                Effect: Allow
                Action:
                  - "s3:GetObject"
                  - "s3:GetObjectVersion"
                  - "s3:GetBucketVersioning"
                  - "s3:PutObject"
                  - "s3:PutObjectAcl"
                  - "s3:PutObjectVersionAcl"
                Resource: 
                  - !GetAtt PipelineBucket.Arn
                  - !Join ['', [!GetAtt PipelineBucket.Arn, "/*"]]
              - 
                Effect: Allow
                Action:
                  - "s3:GetObject"
                  - "s3:GetObjectVersion"
                  - "s3:GetBucketVersioning"
                  - "s3:PutObject"
                  - "s3:PutObjectAcl"
                  - "s3:PutObjectVersionAcl"
                Resource: 
                  - !GetAtt DeployBucket.Arn
                  - !Join ['', [!GetAtt DeployBucket.Arn, "/*"]]
              -
                Effect: Allow
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                  - "cloudfront:CreateInvalidation"
                Resource:
                  - "*"
      Tags:
        - Key: Name
          Value: !Join ['-', [!Ref AppName, !Ref 'AWS::AccountId', 'BuildRole', !Ref Stage]]


  # Create role for CodePipeline
  CodePipeLineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - 
            Effect: Allow
            Principal:
              Service:
                - "codepipeline.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: "2012-10-17"
            Statement: 
              - 
                Effect: Allow
                Action:
                  - "s3:GetObject"
                  - "s3:GetObjectVersion"
                  - "s3:GetBucketVersioning"
                  - "s3:GetObjectAcl"
                  - "s3:PutObject"
                  - "s3:PutObjectAcl"
                  - "s3:PutObjectVersionAcl"                  
                Resource: 
                  - !GetAtt PipelineBucket.Arn
                  - !Join ['', [!GetAtt PipelineBucket.Arn, "/*"]]
              - 
                Effect: Allow  
                Action:
                  - "codebuild:BatchGetBuilds"
                  - "codebuild:StartBuild"
                Resource: "*"
              - 
                Effect: Allow  
                Action:
                  - "codecommit:GetRepository"
                  - "codecommit:GetBranch"
                  - "codecommit:GetCommit"
                  - "codecommit:UploadArchive"
                  - "codecommit:GetUploadArchiveStatus"
                  - "codecommit:CancelUploadArchive"
                Resource: "*"                
      Tags:
        - Key: Name
          Value: !Join ['-', [!Ref AppName, !Ref 'AWS::AccountId', 'PipelineRole', !Ref Stage]]

Enter fullscreen mode Exit fullscreen mode

After the policies we will create our build project for codebuild to use.

  # Create Code Build Project
  CodeBuild:
    Type: 'AWS::CodeBuild::Project'
    Properties:
      Name: !Sub ${AWS::StackName}-CodeBuild
      ServiceRole: !GetAtt CodeBuildRole.Arn
      Artifacts:
        Type: CODEPIPELINE
        Name: MyProject
      Source: 
        Type: CODEPIPELINE
      Environment:
        ComputeType: BUILD_GENERAL1_SMALL
        Type: LINUX_CONTAINER
        Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
      Source:
        Type: CODEPIPELINE
        # This file (buildspec.yml In Source code) contains commands to Create and Push a docker image to the ECR_REPOSITORY_URI
        BuildSpec: buildspec.yml

      Tags:
        - Key: Name
          Value: !Join ['-', [!Ref AppName, !Ref 'AWS::AccountId', 'BuildProj', !Ref Stage]]

Enter fullscreen mode Exit fullscreen mode

We then create our pipeline which has 3 stages source, build and deploy.
Note: For the source declaration, I am using a third-party provider which is GitHub. You will need to generate an auth token from GitHub for AWS to use.

# Create CodePipeline with 3 stages (Source, Build and Deploy)
  CodePipeline:
    Type: 'AWS::CodePipeline::Pipeline'
    Properties:
      RoleArn: !GetAtt CodePipeLineRole.Arn
      Name: !Join ['-', [!Ref AppName, !Ref 'AWS::AccountId', 'CodePipeLine',!Ref Stage]]
      ArtifactStore:
        Location: !Ref PipelineBucket
        Type: S3

      # Stages declaration
      Stages:
     # Download source code from Github Repo to source-output-artifacts path in S3 Bucket
        - 
          Name: Source
          Actions: 
            - 
              Name: SourceAction
              ActionTypeId: 
                 Category: Source
                 Owner: ThirdParty
                 Provider: GitHub
                 Version: 1
              OutputArtifacts: 
                - 
                  Name: MyApp
              Configuration:                
                 Repo: !Ref GithubRepo
                 Branch: !Ref GithubBranch
                 Owner: !Ref GithubUserName
                 OAuthToken: !Ref GithubOAuthToken

        # Build the project using the BuildProject and Output build artifacts to build-output-artifacts path in S3 Bucket
        - 
          Name: Build
          Actions: 
            - 
              Name: BuildAction
              ActionTypeId: 
                Category: Build
                Owner: AWS
                Version: 1
                Provider: CodeBuild
              InputArtifacts: 
                - 
                  Name: MyApp
              OutputArtifacts: 
                - 
                  Name: MyAppBuild
              Configuration:
                ProjectName: !Ref CodeBuild

        # Deploy the project to S3 Bucket for website hosting.
        - 
          Name: Deploy
          Actions: 
            - 
              Name: DeployAction
              ActionTypeId: 
                Category: Deploy
                Owner: AWS
                Version: 1
                Provider: S3
              InputArtifacts: 
                - 
                  Name: MyAppBuild  
              Configuration:                
                BucketName: !Ref DeployBucket
                Extract: 'true'                
      # Create a name tag for the pipeline
      Tags:
        - Key: Name
          Value: !Join ['-', [!Ref AppName, !Ref 'AWS::AccountId', 'CodePipeLine',!Ref Stage]]

Enter fullscreen mode Exit fullscreen mode

Now that we have our IAM Policies, Pipeline and the Build Project, it's time to define our S3 Buckets (Store Pipeline Artifacts, Website Host)

# Create S3 Buckets (Store Pipeline Artifacts, Website Host)
  PipelineBucket: 
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Join ['-', [!Ref AppName, !Ref 'AWS::AccountId', 'pipelineartifacts', !Ref Stage]]

  DeployBucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Join ['-', [!Ref AppName, !Ref 'AWS::AccountId', 'website', !Ref Stage]]
      WebsiteConfiguration:
        IndexDocument: index.html
      AccessControl: PublicReadWrite
      CorsConfiguration:
        CorsRules:
        - AllowedOrigins: ['*']
          AllowedMethods: [GET]

  # Bucket policy that hosts the website
  DeploymentBucketPolicy: 
    Type: AWS::S3::BucketPolicy
    Properties: 
      Bucket: !Ref DeployBucket
      PolicyDocument: 
        Statement: 
          - 
            Action: 
              - "s3:GetObject"
            Effect: "Allow"
            Resource: 
              Fn::Join: 
                - ""
                - 
                  - "arn:aws:s3:::"
                  - 
                    Ref: DeployBucket
                  - "/*"
            Principal: "*"

Enter fullscreen mode Exit fullscreen mode

Upload CloudFormation Template to AWS

Great! Now that we have our CloudFormation template created,
it's now time to upload our template to AWS.

Step 1: Log in to your AWS Console and navigate to CloudFormation; you should see a screen similar to this below. Click on create stack.

Image description

Step 2: Choose upload a template file and upload our cloudformation template.

Image description

Step 3: Give the stack a proper name, and give appropriate values to the stack parameters.

Image description

Step 4: Click next until you're on the last step. Select the checkbox in the last step. This allows AWS to let our template create IAM resources.

Image description

Step 5: Once you have created the stack, you can monitor the events of the stack as it is building. Whenever it is fully completed, we will get a status of 'CREATE_COMPLETE'

Image description

Step 6: Head over to CodePipline and we should see our pipeline in action.

Image description

Step 7: Navigate over to S3 to view the S3 bucket that is hosting our website. Click on the properties tab and scroll all the way to the bottom. You should see a website URL.

Image description

The URL should load our website and you should see the site below.

Image description

That's it, you have built a CI/CD Pipeline that automates your website deployment.

Lastly, to test that the pipeline works properly, push a commit to your git branch. This should automatically trigger the pipeline.

Happy Coding everyone. 👨🏾‍💻

Top comments (2)

Collapse
 
jfoxworth profile image
Joshua Foxworth • Edited

I tried to implement this demo code and got a 404. I've tried doing it with several other tutorials and with just doing it myself. Every single one results in the same 404 error.

Code: NoSuchKey
Message: The specified key does not exist.
Key: index.html

Looking at the pipeline, I see that the build failed.

[Container] 2023/01/23 02:03:56 Selecting 'nodejs' runtime version '16' based on manual selections...
[Container] 2023/01/23 02:03:58 Phase complete: DOWNLOAD_SOURCE State: FAILED
[Container] 2023/01/23 02:03:58 Phase context status code: YAML_FILE_ERROR Message: Unknown runtime version named '16' of nodejs. This build image has the following versions: 10, 12

So, the react code needs version 16 to build, but the template only has 10, 12.

Collapse
 
giyoungjr profile image
Gilbert Young Jr

Hey @jfoxworth appreciate the heads up. This code is outdated certain packages and libraries that were supported when i wrote this article might not be the same. Sorry for any inconvenience.