Creating a Static Site from scratch + AWS S3 + CICD

By The Agile Monkeys

Tai Nguyen Bui
The Agile Monkeys’ Journey
10 min readDec 11, 2018

--

This tutorial will take you through the journey of creating a static website using Gatsby, a static site generator powered by React and GraphQL. Moreover, we will be creating a few very simple tests for a component and a page using Jest and React Testing Library. Finally, we will be deploying our website in an AWS S3 bucket through our CircleCI CICD pipeline.

Before we start, if you have a registered domain that you want to use for your static site, create a bucket named as your domain right now!

If you’re wondering why…

AWS S3 buckets are unique across all AWS regions and AWS Route53 only allows you to create an alias to your S3 bucket if your bucket name and domain name are the same.

For example, my.fakebucket.io -> http://my.fakebucket.io.s3-website-eu-west-1.amazonaws.com

Awesome, now we’re all set and can get started…

1. Creating a static site

As we mentioned before, we are going to create a basic React application using Gatsby, for that we need a few things:

Preparing our environment

We will now proceed to install Gatsby and create our site. If you’d like to use a different name for your site replace “my-static-site” with your own name.

# Install Gatsby CLI
npm install --global gatsby-cli
# Move to your development folder e.g.
cd development
# Create Gatsby Site
gatsby new my-static-site
# Move to your new site folder
cd my-static-site

At this point I highly recommend that you create a new repository in GitHub and commit/push changes as you move forward.

Build and deploy locally

# Building static site
gatsby build
# Deploying locally
gatsby serve

You should now see your static site hosted at http://localhost:9000

Gatsby application home page

We wont be making changes to this page in this tutorial but feel free to make changes and update the tests below.

2. Adding some test

But where would our site be without testing!?

For testing we will be using two main libraries, Jest and React Testing Library. Jest is a testing framework used to test JavaScript code. Among other things, it has been optimized to maximize performance and prints test results nicely. On the other hand, React Testing Library lets you test your React Components being able to treat them as DOM nodes and encouraging good testing practices.

Since Gatsby uses Babel for compiling the JavaScript code, we will have to let Jest know about it, below is explained how we do it.

Setting up Jest testing framework

Following Gatsby Unit Testing tutorial

# Install dev dependencies
npm install --save-dev jest babel-jest react-test-renderer identity-obj-proxy babel-core@^7.0.0-bridge.0 @babel/core babel-preset-gatsby react-testing-library

In order to tell Jest that babel-jest needs to be used for testing, we will need to create a Jest config file in the root of the project named jest.config.js and paste the following configuration information inside:

module.exports = {
"transform": {
"^.+\\.jsx?$": "<rootDir>/jest-preprocess.js"
},
"moduleNameMapper": {
".+\\.(css|styl|less|sass|scss)$": "identity-obj-proxy",
".+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js"
},
"testPathIgnorePatterns": ["node_modules", ".cache"],
"transformIgnorePatterns": ["node_modules/(?!(gatsby)/)"],
"globals": {
"__PATH_PREFIX__": ""
},
"testURL": "http://localhost",
"setupFiles": ["<rootDir>/loadershim.js"]
}

Now create the Babel config file in the root of the project named jest-preprocess.js and paste the following:

const babelOptions = {
presets: ["babel-preset-gatsby"],
}
module.exports = require("babel-jest").createTransformer(babelOptions)

We also need to create another file that will be included before the tests are run. Place it in the root of the project, name it loadershim.js and paste the following into it:

global.___loader = {
enqueue: jest.fn(),
}

Then, let’s create some mock files that will help us test components and pages that use GraphQL or Link.

Create a folder in the root directory called __mocks__ and create a file inside the folder you just created named fileMock.js pasting the following content:

module.exports = "test-file-stub"

Finally, create a file called gatsby.js in __mocks__ folder that will be mocking Gatsby and paste following content:

const React = require('react')
const gatsby = jest.requireActual('gatsby')
module.exports = {
...gatsby,
graphql: jest.fn(),
Link: jest.fn().mockImplementation(({ to, ...rest }) =>
React.createElement('a', {
...rest,
'aria-current': 'page',
className: '',
onClick: Function,
onMouseEnter: Function,
href: to,
})
),
StaticQuery: jest.fn(),
}

Creating our first tests

Now that everything is setup, let’s write our first test. It is up to you where you place the tests but for this tutorial we’ll create the tests files under __test__ folder in the root of the project.

Since this tutorial is more about creating some basic tests for the pipeline, let’s test that a component renders successfully and that the content of a page is correct.

# Test a Component
This basic test will check that the Header renders correctly.

Create a test file in __test__/components named header.test.js and paste the following code:

import React from 'react'
import renderer from 'react-test-renderer'
import Header from '../../src/components/header'
describe('Header', () => {
it('renders correctly', () => {
const tree = renderer.create(<Header />).toJSON()
expect(tree).toMatchSnapshot()
})
})

# Test a Page
This test will check the content of the 404 page.

Create a test file in __test__/pages named 404.test.js and paste the following code:

import React from 'react'
import { render } from 'react-testing-library'
import { StaticQuery } from 'gatsby'
import NotFound from '../../src/pages/404'
beforeEach(() => {
StaticQuery.mockImplementationOnce(({ render }) =>
render({
site: {
siteMetadata: {
title: `GatsbyJS`,
},
},
})
)
})
describe(`404`, () => {
it(`contains NOT FOUND text`, () => {
const { getByText } = render(<NotFound />)
const element = getByText(`NOT FOUND`)expect(element).toBeInTheDocument
})
it(`contains NOT FOUND message`, () => {
const { getByText } = render(<NotFound />)
const element = getByText(
`You just hit a route that doesn't exist... the sadness.`
)
expect(element).toBeInTheDocument
})
})

We are now ready to run the tests we just created…

Running tests

Update the test script in package.json by replacing the value of test to the following:

"scripts": {
...
"test": "jest"
...
}

and then run the tests:

npm test

3. Adding Serverless Framework to project

Now we have a working static site with some basic tests! Let’s add the Serverless Framework so we can automate deployments when we merge to master.

First of all, let’s install the Serverless CLI

npm install -g serverless

Then add the Serverless-Finch dependency to the project

npm install --save-dev serverless-finch

This dependency will make things easier for us to deploy our static site to an S3 bucket

Create a file in the root of the project named serverless.yml and paste the configuration below. Bear in mind that you will need to update region and sitename. It is important that you choose the right region from the beginning and stick to it. If you wish to change region later on you will need to delete the bucket and update the region. This is due to the fact that S3 buckets are unique across all AWS regions.

service: <your site name>provider:
name: aws
runtime: nodejs8.10
region: <the region for your bucket e.g. us-east-1>
stage: dev
plugins:
- serverless-finch
custom:
siteName: <your domain or bucket name>
client:
bucketName: ${self:custom.siteName}
distributionFolder: public
resources:
Resources:
StaticSite:
Type: AWS::S3::Bucket
Properties:
AccessControl: PublicRead
BucketName: ${self:custom.client.bucketName}
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: 404.html
# specifying the policies to make sure all files inside the Bucket are avaialble
WebAppS3BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket:
Ref: StaticSite
PolicyDocument:
Statement:
- Sid: PublicReadGetObject
Effect: Allow
Principal: '*'
Action:
- s3:GetObject
Resource: arn:aws:s3:::${self:custom.client.bucketName}/*

IAM User (Skip if already configured)

In order to be able to deploy from your machine you will need to have an AWS IAM profile setup in your computer. Follow this tutorial if you do not have it yet: Set up AWS Credentials

Deploying or Undeploying static site

Add the following name/value pairs under scripts to package.json


“scripts”: {

“deploy”: “npm run-script build && sls client deploy — no-confirm”,
“undeploy”: “sls client remove”

}

Just in case, run npm install to make sure that all our dependencies are installed and finally deploy:

# deploy
npm run-script deploy
# undeploy
npm run-script undeploy

Once deployed, the output should be something like:

Serverless: Success! Your site should be available at http://<your bucket name>.s3-website-<your region>.amazonaws.com/

This is great! we have now a site that we can easily deploy to an S3 bucket with just a couple of commands. In the next session we will be able to fully automate this process.

4. Creating an IAM User for CICD

We will be using CircleCI for our CICD pipeline because it is free and works great.

In order to avoid using our credentials for the pipeline, we will create an IAM User that will only have access to the S3 Bucket in which we will deploy our site.

Go to AWS Console and click AWS IAM. Once you are the AWS IAM page, click Users and Add user. Type a meaningful name to that user,
e.g. circleci-static-site-deployment

and tick only Programmatic access

click Next

You should now be in the Permissions page. We’ll have to create a new custom policy so first click Attach existing policies directly and then click Create policy.

We will select S3 service and setup a policy that will give full access only to our S3 bucket. The name that you specified for the S3 bucket in the serverless.yml will be needed.

Make sure you set a specific bucket in the resources.

Then, click Review policy and put a name for the policy, e.g. circleci-static-site-deployment-gatsby-demo.

Now that the policy has been created, we will go back to the permissions page and refresh, we will then see our new policy available.

Continue by clicking Next and then Create user and download the credentials provided. They will be used when setting up CircleCI.

5. Setting up CICD

As mentioned before, We will be using CircleCI for our CICD pipeline because it is free and works great.

From now, we will assume you have linked your GitHub account to CircleCI and that you can see your project.

Login into your CircleCI account and go to Projects tab.

Click Set Up Project, select macOS as the Operating System and follow the instructions.

Paste the following code in .circleci/config.yml

defaults: &defaults
working_directory: ~/<the name if your repository>
docker:
- image: circleci/node:8.10
version: 2
jobs:
build:
<<: *defaults
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "package-lock.json" }}
- run:
name: Install dependencies
command: |
npm install
- save_cache:
paths:
- node_modules
key: v1-dependencies-{{ checksum "package-lock.json" }}
- run: npm run-script buildtest:
<<: *defaults
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "package-lock.json" }}
- run:
name: Install dependencies
command: |
npm install
- save_cache:
paths:
- node_modules
key: v1-dependencies-{{ checksum "package-lock.json" }}
- run: npm test -- -udeploy:
<<: *defaults
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "package-lock.json" }}
- run:
name: Install Serverless CLI and dependencies
command: |
sudo npm install -g serverless
npm install
- save_cache:
paths:
- node_modules
key: v1-dependencies-{{ checksum "package-lock.json" }}
- run: npm run-script build
- run: npm run-script deploy
- store_artifacts:
path: public
- store_artifacts:
path: serverless.yml
workflows:
version: 2
build-test-deploy:
jobs:
- build
- test:
requires:
- build
- deploy:
filters:
branches:
only:
- master
requires:
- test

The above CircleCI Workflow is divided into three steps. Build, Test and Deploy. Moreover, the deployment is only triggered when Tests pass and files are being merged into Master.

Then, click Start building

and push the latest changes to your Git Repository.

We will probably see at this point that the Deployment is failing due to permissions. This is because we have not yet setup the AWS Credentials we previously created. So, let’s set it up now.

Go to your project settings in order to create two environment variables, AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. The value for each of the keys will be the ones that we saved previously when we created the IAM User for CircleCI.

6. Deploying our first change through the Pipeline

Everything is now setup. If you want to successfully deploy your static site through your CICD Pipeline, the only thing you need is to either re-run the job that failed previously due to missing AWS Credentials or pushing a new change to master.

Congratulations! We are done!

In case you want to have a look at the code of the project that was generated for this tutorial, you can find it in our Github Repo

--

--

Tai Nguyen Bui
The Agile Monkeys’ Journey

Software Engineer @theagilemonkeys and passionate about Tech and Motorbikes