Automated static website deployments via AWS and GitHub

Use GitHub to trigger AWS CodePipeline and CodeBuild to generate your static website onto S3.

In this post I want to go over how I automated the build and deployment process of this website. This whole process is mapped and controlled by three main files: cfn-stack.json, buildspec.yml and build.js (all files in this blog will be linked to the most recent commit at the time of writing (e10a972), check master for updates).

Step 1 AWS CloudFormation

AWS CloudFormation gives developers and systems administrators an easy way to create and manage a collection of related AWS resources, provisioning and updating them in an orderly and predictable fashion.

As a general rule, every project I deploy gets its own CloudFormation template document. It takes a little longer at the beginning of a project to get all setup but you'll thank yourself later when you come back to it six months down the line. It makes resources and configuration so much easier to keep track of. The entire stack for this site is all here in cfn-stack.json. Feel free to use this template for your own static website. From the AWS CloudFormation dashboard all you have to do is upload the template, appropriately fill in the template parameters and submit. In a short while later you'll have everything from DNS configuration to code deployment all setup and ready for you. I'm not going to breakdown the document in full detail but I'll highlight the main sections and their functions:

Most of the configuration parameters needed for this template are from GitHub. You need your GitHub username, the name of the repo for your static site and the name of the branch that will trigger CodePipeline to build (probably master). You'll also need to create a "Personal Access Token" in your account settings here. The only other parameter you need is the domain of your static site (non www) e.g. "dadoune.com".

Step 2 AWS CodeBuild Build Specification

Every time a build gets triggered, CodePipeline downloads the source and runs through the build as defined by the buildspec.yml located in the root of the project. Full documentation on the buildspec.yml file can be found here. On line 142 of cfn-stack.json, I set the build container to be ubuntu-base:14.04, so anything you can do with Ubuntu you can do with for your build. Here is my whole buildspec:

version: 0.1
phases:
  install:
    commands:
      # Install nodejs https://nodejs.org/en/download/package-manager/
      - curl -sL https://deb.nodesource.com/setup_7.x | bash -
      - apt-get install -y nodejs
      # Install yarn natively https://yarnpkg.com/en/docs/install#linux-tab
      - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
      - echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
      - apt-get update
      - apt-get install -y yarn
  pre_build:
    commands:
      - yarn
  build:
    commands:
      - yarn run build
  post_build:
    commands:
      - aws s3 sync --delete --exclude assets/* build/ "s3://${BUCKET_NAME}"
      - aws s3 sync --delete --cache-control "max-age=31536000" build/assets "s3://${BUCKET_NAME}/assets"

Most of that should be pretty straight forward:

  1. Install NodeJS and yarn.
  2. Run yarn to install all of the needed NPM packages defined in package.json.
  3. Run yarn build defined here, which generates the static site with Metalsmith.
  4. Sync the static site S3 bucket with the newly generated build.

Sync Elaboration

You'll notice I sync twice during post_build, the first one syncs all of the non asset files with no "cache-control" meta because I don't want them cached by CloudFront. The second one syncs all of my asset files and sets their "max-age" to one year, CloudFront will cache them based on this value. All of my asset file names are generated with a fingerprint hash during the Metalsmith build portion so I have no cache bust concerns. The other thing you might notice about this section is the use of the environment variable BUCKET_NAME, this value is defined in my CloudFormation template here. Also, the --delete flag means that sync will delete any files in the bucket that are not in the build folder.

Step 3 Static Site Generation

This step actually occurs in the middle of step 2 but I'm calling it step 3 anyways. In my particular case this is where I have Metalsmith generate my site but you could use any static site generation framework for this step really. A few other popular frameworks are Jekyll and Hugo. The intricacies of my build are a bit out of scope for what I want to cover in this blog but you can check my source out [here](build.js.