Advanced React continuous deployment pipeline from Github to S3 with AWS CodePipeline and AWS CodeBuild

0
779
CloudFormation template

Introduction

This CloudFormation template will create a stack for a full deployment pipeline for your React app with AWS CodePipeline and AWS Codedeploy.

The pipeline will react to git push performed to a branch on a Github repository and it will build the React project and upload it to an S3 static website. It will also create a custom domain name in a Route53 hosted zone, a free SSL certificate in AWS Certificate Manager and it will create a CloudFront distribution with the SSL certificate and custom domain name already configured in the distribution.

The template contains everything you need to setup this deployment pipeline:

  • 2 S3 buckets (one for codepipeline artifacts and one for the static site – configured with static website hosting and the necessary bucket policy)
  • An AWS CodePipeline preconfigured with everything needed to run the pipeline
  • An AWS CodeDeploy build project that will build your React app (if you want just a static html site this can be skipped)
  • 2 IAM Roles (one for AWS CodePipeline and one for AWS CodeBuild) configured with the necessary permissions
  • A custom domain name in a Route 53 hosted zone
  • An SSL certificate in Certificate Manager for your custom domain name
  • A CloudFront distribution with custom domain name and SSL certificate configured

If you need to configure this without a custom domain name, without an SSL certificate and without CloudFront CDN – so just a simple S3 static website then check out this template which contains that setup.

Configuration

When you upload the template to CloudFormation and try to create the stack you will be prompted to add the following information:

  1. Branch. This is the branch from your github repo that you want to deploy
  2. Repository owner. This your github user name and the second part of the repo url (bold underlined): https://github.com/majestic-cloud/docs
  3. Repository. This is the project name. This is the last part of your github repo url (bold underlined): https://github.com/majestic-cloud/docs
  4. GithubOAuthToken. This is your github personal access token. To get this sign in to your Github account then click on your user’s icon in the top right part of the screen, then go to Settings -> Developer settings -> Personal access tokens (or you can go directly to this link: https://github.com/settings/tokens) and then click on Generate new token. After you configure the permissions (check the first checkbox named repo that gives access to all the “repo” permissions) paste the obtained token into the CloudFormation console.
  5. StaticSiteDomain. This is the domain name you want to use for the static website. It will get an SSL certificate automatically if your main domain is hosted in Route53.
  6. HostedZone. This is the name of your hosted zone. That is the main domain of your site. So if you deploy the react app to myreactapp.example.com then this is example.com and it should use Route53 for this to work.
  7. HostedZoneID. This is the ID of your hosted zone from Route53. Go to the Route53 console and in the list of the hosted zones you can find the ID to the right of your desired domain (which should use Route53)

Deployment

You need to go to CloudFormation, create a new stack with the template below and fill in the required fields before creating the stack.

The template

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  Branch:
    Type: String
    Default: "Type here your branch name (usually master or main)"
  RepoOwner:
    Type: String
    Default: majestic-cloud
  Repository:
    Type: String
    Default: myapp
  GithubOAuthToken:
    Type: String
    Description: "Github access token"
  StaticSiteDomain:
    Type: String
    Description: "The domain name to be used with this static site"
    Default: "myreactapp.majestic-cloud.com"
  HostedZone:
    Type: String
    Description: "The hosted zone in which we will create a subdomain"
    Default: "majestic-cloud.com"
  HostedZoneID:
    Type: String
    Description: "The ID of your hosted zone in Route53"
    Default: "ZZZZZZZZZZZZZZ"
Resources:
  ReactToS3CodePipeline:
    Type: 'AWS::CodePipeline::Pipeline'
    Properties:
      RoleArn: !GetAtt CodePipeLineRole.Arn
      ArtifactStore:
        Location: !Ref PipelineBucket
        Type: S3
      Stages:
        - 
          Name: Source
          Actions: 
            - 
              Name: SourceAction
              ActionTypeId: 
                Category: Source
                Owner: ThirdParty
                Provider: GitHub
                Version: 1
              OutputArtifacts: 
                - 
                  Name: TheApp
              Configuration:
                Owner: !Ref RepoOwner
                Repo: !Ref Repository
                Branch: main
                OAuthToken: !Ref GithubOAuthToken
        - 
          Name: Build
          Actions: 
            - 
              Name: BuildAction
              ActionTypeId: 
                Category: Build
                Owner: AWS
                Version: 1
                Provider: CodeBuild
              InputArtifacts: 
                - 
                  Name: TheApp
              OutputArtifacts: 
                - 
                  Name: TheBuiltApp
              Configuration:
                ProjectName: !Ref CodeBuild
        -
          Name: Deploy
          Actions:
            -
              Name: DeployAction
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: 1
                Provider: S3
              InputArtifacts:
                -
                  Name: TheBuiltApp
              RunOrder: 1
              Configuration:
                BucketName: !Ref DeployBucket
                Extract: true
  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"
                  - "s3:DeleteObject"
                Resource: 
                  - !GetAtt PipelineBucket.Arn
                  - !Join ['', [!GetAtt PipelineBucket.Arn, "/*"]]
              - 
                Effect: Allow
                Action:
                  - "s3:GetObject"
                  - "s3:GetObjectVersion"
                  - "s3:GetBucketVersioning"
                  - "s3:PutObject"
                  - "s3:PutObjectAcl"
                  - "s3:PutObjectVersionAcl"
                  - "s3:DeleteObject"
                Resource: 
                  - !GetAtt DeployBucket.Arn
                  - !Join ['', [!GetAtt DeployBucket.Arn, "/*"]]
              -
                Effect: Allow
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                  - "cloudfront:CreateInvalidation"
                Resource:
                  - "*"
  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:PutObject"
                  - "s3:PutObjectAcl"
                  - "s3:PutObjectVersionAcl"
                  - "s3:DeleteObject"
                Resource: 
                  - !GetAtt PipelineBucket.Arn
                  - !Join ['', [!GetAtt PipelineBucket.Arn, "/*"]]
              -
                Effect: Allow
                Action:
                  - "s3:GetObject"
                  - "s3:GetObjectVersion"
                  - "s3:GetBucketVersioning"
                  - "s3:PutObject"
                  - "s3:PutObjectAcl"
                  - "s3:PutObjectVersionAcl"
                  - "s3:DeleteObject"
                Resource:
                  - !GetAtt DeployBucket.Arn
                  - !Join ['', [!GetAtt DeployBucket.Arn, "/*"]]
              - 
                Effect: Allow  
                Action:
                  - "codebuild:BatchGetBuilds"
                  - "codebuild:StartBuild"
                Resource: "*"
  CodeBuild:
    Type: 'AWS::CodeBuild::Project'
    Properties:
      Name: !Sub ${AWS::StackName}-CodeBuild
      ServiceRole: !GetAtt CodeBuildRole.Arn
      Artifacts:
        Type: CODEPIPELINE
        Name: MyProject
      Environment:
        ComputeType: BUILD_GENERAL1_SMALL
        Type: LINUX_CONTAINER
        Image: "aws/codebuild/amazonlinux2-x86_64-standard:2.0"
      Source:
        Type: CODEPIPELINE
        BuildSpec: !Sub |
          version: 0.1
          phases:
            pre_build:
              commands:
                - echo This will run npm install to install the dependencies...
                - npm install
            build:
              commands:
                - echo Actually run the build process
                - npm run build
          artifacts:
            files:
              - '**/*'
            base-directory: build
  PipelineBucket: 
    Type: 'AWS::S3::Bucket'
    Properties: {}
    DeletionPolicy: Retain
  DeployBucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      AccessControl: PublicRead
      WebsiteConfiguration:
        IndexDocument: index.html
        ErrorDocument: error.html
    DeletionPolicy: Retain
  DeployBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      PolicyDocument:
        Id: MyPolicy
        Version: 2012-10-17
        Statement:
          - Sid: PublicReadForGetBucketObjects
            Effect: Allow
            Principal: '*'
            Action: 's3:GetObject'
            Resource: !Join
              - ''
              - - 'arn:aws:s3:::'
                - !Ref DeployBucket
                - /*
      Bucket: !Ref DeployBucket
  ACMCertificate:
    Type: "AWS::CertificateManager::Certificate"
    Properties:
      DomainName: !Ref StaticSiteDomain
      DomainValidationOptions:
        - DomainName: !Ref StaticSiteDomain
          HostedZoneId: !Ref HostedZoneID
      ValidationMethod: DNS
  CloudFrontDistribution:
    Type: "AWS::CloudFront::Distribution"
    Properties:
      DistributionConfig:
        Origins:
          - 
            DomainName: !GetAtt DeployBucket.RegionalDomainName
            Id: !Ref DeployBucket
            S3OriginConfig:
              OriginAccessIdentity: ''
        DefaultRootObject: index.html
        Enabled: true
        Aliases:
          - !Ref StaticSiteDomain
        DefaultCacheBehavior: 
          MinTTL: 86400
          MaxTTL: 31536000
          ForwardedValues: 
            QueryString: true
          TargetOriginId: !Ref DeployBucket
          ViewerProtocolPolicy: "redirect-to-https"
        ViewerCertificate:
          AcmCertificateArn: !Ref ACMCertificate
          MinimumProtocolVersion: TLSv1.1_2016
          SslSupportMethod: sni-only
        CustomErrorResponses:
        - ErrorCode: '403'
          ErrorCachingMinTTL: '300'
          ResponseCode: '200'
          ResponsePagePath: "/index.html"
        - ErrorCode: '404'
          ErrorCachingMinTTL: '300'
          ResponseCode: '200'
          ResponsePagePath: "/index.html"
  DomainAliasForSite:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneName: !Join [ "", [ !Ref HostedZone, "." ] ]
      Name: !Ref StaticSiteDomain
      Type: A
      AliasTarget:
        DNSName: !GetAtt CloudFrontDistribution.DomainName
        HostedZoneId: Z2FDTNDATAQYW2

Find the above template on Github: https://github.com/majestic-cloud/react-deployment-pipeline/blob/main/cfn-templates/reactToS3v2.yaml

Leave A Reply

Please enter your comment!
Please enter your name here