Js App Continuous Deployment (for Every Branch) Using CircleCI & S3

Recently at Viget, we've been developing a number of highly interactive client-side applications. We've long been believers in continuous integration and continuous deployment, which typically involves the CI service (CircleCI, in our case) running a Capistrano deployment to push the latest updates to the integration environment after all the other checks pass.

What's cool with a client-side-only app is that your app compiles down to a series of static files, so your "integration environment" can be a file hosting solution like Amazon's S3. And what's EXTRA cool is that this technique allows for continuous deployment for every branch you push up, without having to deal with database discrepencies, managing virtual hosts, etc. Interested? Good. Let's see how it's done.

Create an S3 Bucket

First, let's create an AWS S3 bucket with the proper configuration for hosting our app:

  1. Log In and navigate to S3 settings
  2. Click "Create bucket"
  3. Give it a name ("example-bucket"), and a region ("US East (N. Virginia)")
  4. Click "Next" and keep clicking "Next" to finish the bucket creation
  5. Click on the bucket and go to the "Properties" tab
  6. Click "Static website hosting" and choose "Use this bucket to host a website"
  7. Set the index document: index.html
  8. Click "Save"
  9. Go to the "Permissions" tab, then the "Bucket Policy" tab, and fill in:
    {
    "Id": "Policy1483481703857",
    "Statement": [
        {
            "Sid": "Stmt1483481698457",
            "Effect": "Allow",
            "Principal": "",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::example-bucket/"
        }
    ]
    }
    
  10. Click "Save"

Configure Application AWS Credentials

Next, let's set up our application's scripts and configuration for deploying to our newly created S3 bucket. We'll want to keep our AWS credentials out of the repository. Create a .env.example file with the following:

AWS_DEFAULT_REGION=us-east-1
AWS_ACCESS_KEY_ID=REPLACE_ME
AWS_SECRET_ACCESS_KEY=REPLACE_ME
S3_BUCKET_URL=REPLACE_ME

Then, add .env to .gitignore (echo .env >> .gitignore), copy the example file over (cp .env.example .env), and then update .env to include the proper AWS credentials.

Create Deploy Scripts

Next, let's create a npm/yarn script (build is just for example and not necessary). Add the following to package.json:

{
  ...
  "scripts": {
    ...
    "build": "mkdir -p build && cp index.html build/",
    "deploy": "bash -e tasks/deploy.sh $BRANCH_NAME"
  }
}

And create a deploy script (tasks/deploy.sh):

# load the .env file if deploying from a dev machine (not CI)
if [ ! "$CIRCLECI" == true ]; then
  source .env
fi

# make the values from .env available to the following commands
export AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY
export AWS_DEFAULT_REGION
export NODE_ENV=production

yarn build

# Deploy to S3 bucket
aws s3 sync build/ $S3_BUCKET_URL$BRANCH_NAME

Note $BRANCH_NAME = '' on CircleCI for master branch which will deploy master to the root of your bucket.

Configure CircleCI

Now let's configure CircleCI to deploy each branch to a nested folder under root on our S3 bucket.

  1. Configure CircleCI to build your project.
  2. Go to Settings -> Environment Variables and add the same values that you're using in your .env file:
  3. Add/modify a circle.yml config file:
# circle.yml

dependencies:
  override:
    - yarn
  cache_directories:
    - ~/.cache/yarn

deployment:
  integration:
    branch: master
    commands:
      - yarn deploy
  feature:
    branch: /^(?!master).*$/
    commands:
      - BRANCH_NAME=$CIRCLE_BRANCH yarn deploy

The last conditional is to set the BRANCH_NAME ENV variable for non-master branches.

BONUS! Slack Notifications

It's dirt simple to configure your deploy script to post a message in Slack with the URl of the newly deployed branch. From within the Slack app:

  1. Click the Settings button, then "Add an app or integration", then "Manage" up at the top
  2. Click "Custom Integrations"
  3. Click "Incoming Webhooks"
  4. Click "Add configuration"
  5. Select the channel you want to notify from the drop-down.
  6. Click "Add Incoming WebHooks integration"
  7. Customize Name (optional): Deploybot
  8. Customize Icon (optional):
  9. Copy & paste the generated Webhook URL into ./tasks/deploy.sh
aws s3 sync build/ $S3_BUCKET_URL$BRANCH_NAME && \
  curl -X POST -d "{\"text\":\"Just deployed $BRANCH_NAME to \"}" https://hooks.slack.com/services/YOUR_SLACK_WEBHOOK_TOKEN_HERE

BONUS 2! Example README Documentation

## Deployment

The application is deployed to an AWS S3 bucket. The credentials and bucket are configured in `.env`. To assist, run:

```
cp .env.example .env
```

Next replace all placeholder values in `.env` with valid AWS credentials. See 1Password for account details.

Then, install the AWS CLI tool.

On OSX:

```
brew install awscli
```

To deploy:

```
yarn deploy
```

To manually deploy a specific branch, check out that branch and then run:

```
BRANCH_NAME=feature-branch-name yarn deploy
```

On success, the branch name will be available at a nested directory (under root) with the name you specified. For example: http://example-bucket.s3-website-us-east-1.amazonaws.com/feature-branch-name
Mike Ackerman

Mike is a developer who uses his computer and electrical engineering degrees to craft complex but streamlined back-end systems for clients such as Shure and Privia Medical Group.

More articles by Mike
  • Articles
  • Js App Continuous Deployment (for Every Branch) Using CircleCI & S3