Deploying static sites to S3 with Circle CI

The specifics here are illustrated using a Jekyll site, but it should be trivial to substitute your static site generator of choice.

A publishing script

We’re going to start with a publishing script. This is something that would make life simpler for a team sharing this with or without a CI tool deploying the site, and for a single user as well. The script is going to target our S3 bucket and use the AWS CLI tool to sync the contents of the build directory to the bucket.

The script is also going to specify the AWS profile to use. If you work with multiple AWS accounts the AWS CLI lets you configure different profiles with respective credentials, so you can access business, client, or personal accounts from the same system user account. This script presumes there’s an existing AWS profile named ‘companyprofile’.

#!/usr/bin/env bash
DEFAULT="companyprofile"
PROFILE=${AWS_PROFILE:-$DEFAULT}
BUCKET=my-s3-bucket
DIR=_site/
aws  s3  sync $DIR s3://$BUCKET/ --profile "$PROFILE"

The profile here is what we’d expect the developer or anyone else on the team to be using from their local environments. It’s trivial to change this via an environment variable in the CI environment (or a local environment).

The point of the script is to make it dead simple for a person to publish and to allow a CI server to do the same using the same script.

Use a distinct IAM user

If you’re a new AWS user you may be tempted to just log into the AWS console and grab your API credentials - DO NOT DO THIS. You’re going to create a user just for this deployment with its own credentials for limiting access.

All you need to do is create the user via the IAM console. Go to Identity and Access Management, ‘Users’, and then create new.

Make sure you download or copy the credentials after you create the account, and then hold on to them. We’re not going to set any permissions or groups for this user, so don’t worry about any of that. We’ll use a bucket policy for access management instead - more on that below.

Configuring Circle

Circle CI is primarily a build tool, but we’re going to take advantage of its deployment capabilities. The main benefit to using a CI service or other remote deployment service is that it makes it easier for a larger team to work on a project. You only need to provide access to the source repository, and then let your automated tooling do the rest.

You configure your build with a YAML file. Here’s our stripped down build definition.

dependencies:
    override:
        - bundle install
        - sudo pip install awscli

deployment:
    aws:
    branch: master
        commands:
        - jekyll build
        - /bin/bash publish.sh

You should also add your new AWS user’s credentials to your Circle account. Luckily there’s a feature just for handing AWS credentials.

Open your project settings and find the link for “AWS Credentials”. Enter the AWS key id and secret key that you downloaded earlier here. With the publishing script presented here you’d need to specify a different AWS profile in your environment, and you can do that in either the Circle build configuration file or in the project settings.

Creating your bucket

You can create your bucket from the AWS management console. The management console will let you set up static web hosting as well.

Static hosting config

Select ‘Enable website hosting’ and update the index and error document fields. Almost done here.

Adding a bucket policy

The new user won’t be able to add anything to the bucket, and you won’t be able to see the site publicly without a bucket policy. This is the permission policy that governs who can do what for a given resource or set of resources, in this case our bucket and its contents.

The policy needs to allow 3 things:

  1. For anyone to be able to get an object in the bucket
  2. For the CI user to be able to list the bucket’s contents
  3. For the CI user to be able to modify objects in the bucket (get, add, delete)

The presumption here is that any user you might be using locally has the rights via a user permission or group. If not, and you want to deploy locally as well, then you’ll need to add permissions for additional users to your bucket policy.

{
  "Id": "Policy1421785248746",
  "Statement": [
    {
      "Sid": "Stmt1421785119791",
      "Action": [
        "s3:GetObject"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::my-s3-bucket/*",
      "Principal": {
        "AWS": [
          "*"
        ]
      }
    },
    {
      "Sid": "Stmt1421785171613",
      "Action": [
        "s3:ListBucket"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::my-s3-bucket",
      "Principal": {
        "AWS": [
          "arn:aws:iam::accountId:user/circle-s3-user"
        ]
      }
    },
    {
      "Sid": "Stmt1421745246135",
      "Action": [
        "s3:DeleteObject",
        "s3:GetObject",
        "s3:GetObjectVersion",
        "s3:PutObject"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::my-s3-bucket/*",
      "Principal": {
        "AWS": [
          "arn:aws:iam::accountId:user/circle-s3-user"
        ]
      }
    }
  ]
}

This is based on a policy created using the AWS Policy Generator tool.

Hey! Before you go, checkout my book, Django Standalone Apps! It's a bit rough around the edges as I'm writing it in public, which means chapters get released periodically. But I'd love if you'd check it out (it's free to read online).