Simple CICD with CodeBuild, Github, and S3 for Single Page Applications

Goal

The goal of this is to create a simple CI/CD system to build and deploy an SPA (Single Page App) to AWS S3.

Requirements

  1. Development branch should deploy to development environment.
  2. A release from the Master branch should deploy to production environment.
  3. Build, test, and deploy in a single place.

Tools

  1. AWS CDK.
  2. TypeScript.

Prerequisites

  1. Familiarity with AWS CDK.
    1. Create a typescript project. See this to get started with typescript and the AWS CDK.
    2. Be familiar with this to the point where you can update your stack located at lib/your-stack.ts.
  2. Familiarity with Typescript.

CodeBuild vs Local Builds

This is the only thing you'll need to deploy manually. This CodeBuild environment becomes the new build environment for your SPA.

If you've been deploying from your local machine this effectively replaces that environment for one that is always available in AWS.

Code

Using AWS CDK you can create the CodeBuild environment by updating your stack inside the lib directory.

import * as cdk from '@aws-cdk/core'
import * as codebuild from '@aws-cdk/aws-codebuild'
import * as iam from '@aws-cdk/aws-iam'

export class CiCdStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    // Define some basics information.
    const org = 'YourGithubOrgName'
    const repo = `YourRepoName`
    const develop = 'YourDevelopmentBranchName'
    // This is the builds spec that is associated with the project being built. This is where you'll deploy to S3 from.branchOrRef: '*', // * Covers all branches, tags, commit IDs, etc...
    const buildSpec = 'buildspec.yaml'
    const releaseFilter = "^refs\/tags\/v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*).*$"

    // codeBuildIamPrincipal is shared across stacks.
    const codeBuildIamPrincipal = 'site-publisher'

    // Define the source details.
    const gitHubSource = codebuild.Source.gitHub({
      owner: org,
      repo: repo,
      webhook: true,
      // * Covers all branches, tags, commit IDs, etc...
      branchOrRef: '*',
      webhookFilters: [

        // Runs build on release from any target branch (normally master).
codebuild.FilterGroup.inEventOf(codebuild.EventAction.PUSH)
          .andHeadRefIs(`${releaseFilter}`),

        // Runs build on a push to develop branch.
codebuild.FilterGroup.inEventOf(codebuild.EventAction.PUSH)
          .andBranchIs(develop)
      ],
      webhookTriggersBatchBuild: true
    })

    // Create a role for our Codebuild so it can be used by other stacks.branchOrRef: '*', // * Covers all branches, tags, commit IDs, etc...
    const sitePublisherCodeBuild = new iam.Role(this, 'Role', {
      assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
      roleName: codeBuildIamPrincipal
    })

    // Setup the Codebuild project.
    new codebuild.Project(this, org, {
      source: gitHubSource,
      buildSpec: codebuild.BuildSpec.fromSourceFilename(buildSpec),
      role: sitePublisherCodeBuild
    })
  }branchOrRef: '*', // * Covers all branches, tags, commit IDs, etc...
}

Deploy

Once your code is setup just deploy it to AWS.

npm run build
cdk synth
cdk deploy

When this is completed you'll have a CodeBuild project you should be able to see in the AWS Console.

Webhook

Once this is deployed a webhook between the CodeBuid project and Github should be setup. Take a look at your GitHub repo under settings/hooks and you'll see a new webhook was setup for you.

This is a lot better than it used to be. You used to have to get the URL and Secret from the UI and manually enter it into the GitHub settings.

The buildspec

In your buildspec.yaml add the details you need to perform the build and deployment.

In this case we're building a GatsbyJS project.

version: 0.2

phases:
  install:
    runtime-versions:
      nodejs: 14
    commands:
      - npm install -g gatsby-cli
      - npm --version

  pre_build:
    commands:
      - export FRONTEND_ROOT=${CODEBUILD_SRC_DIR}/PathToYourFrontEndRoot
      - cd ${FRONTEND_ROOT}branchOrRef: '*', // * Covers all branches, tags, commit IDs, etc...
      - echo Installing dependencies...
      - npm install
  build:
    commands:
      - npm run build
  post_build:
    commands:
      - echo Build completed deploying to hosting bucket...
      - npm run deploy

batch:
  fast-fail: true
  build-list:
    - identifier: linux_small
      env:
        compute-type: BUILD_GENERAL1_SMALL
      ignore-failure: false

Build and Deploy Scripts

Inside the package.json file of the project being built you'll need to handle these as if you're deploying them from your local environment.

"scripts": {
    ...,
    "build": "gatsby build",
    "deploy": "aws s3 cp public/ s3://Your-Hosting-Bucket-Name/ --recursive",
    ...
  },

Usage

Now once you cut a release where the tag looks like v0.0.1 in Github and CodeBuild will be triggered.

Pushing or merging to your development branch will trigger CodeBuild too.

16