A few weeks ago I started thinking about how to deploy my new side project. The project consists of a pretty small API written with rails, and a completely separate static React application.

For deploying the API I wrote an ansible role that basically replicates the capistrano approach to deploying code. Basically it checks out the latest code from master into a new folder, symlinks in a bunch of persistent files/configuration, and then symlinks the current application directory to the newly checked out code. This approach allows for fast rollback and easy troubleshooting. This part was pretty straight forward, and not very interesting.

For the static react application I wound up doing something and kind of awesome. Instead of running a static server with nginx, I decided to try a serverless approach that utilizes S3 and CloudFront. The serverless approach is extremely scalable, extremely cheap, and requires 0 maintenance.

Initially I wanted to just host things out of s3, this seemed like an easy/practical thing to do. But I quickly learned that I wouldn't be able to use TLS for my domain in the way that I wanted, so I started research how to make it work. It turns out that AWS has CloudFront (a CDN) which can act as a proxy in front of S3, and allows you to do TLS via Server Name Indication.

So I quickly tested things out by manually uploading things to an s3 bucket, and hooking up CloudFront, and I was immediately pretty satisfied, but then I tried to update some files and realized that the caching with CloudFront was going to be an issue. One way I had heard of dealing with this in the past is to add an md5/sha2 suffix to each file, and then every time anything is updated a new pointer is created, which allows users with old/cached versions of things to continue working while new requests get an all-new set of assets.

So I started trying to figure out how I was going to achieve that behavior. After some googling I found some awesome gulp packages that let me achieve exactly what I wanted!

How it works

  1. gulp dist -- This compiles/builds all of the static assets (less, jsx, js, etc) into a single main.css and main.js and the output is a directory that is deployable.
  2. gulp rev-all This goes through the directory that was outputted by step 1, adds a sha/md5 suffix to every file, and updates all references to the old paths to the new paths with the sha/md5 in the filenames.
  3. Publish to the S3 Bucket -- all new files will be uploaded, all existing files that have not changed will be left alone, and all changed files (like index.html) will be updated in s3.
  4. Invalidate the cache -- Once the new files are in s3, I invalidate a single file in the CloudFront distribution /index.html
  5. Wait for 10 minutes, and your changes will be globally deployed across all continents providing a nice/fast experience to the user.

I've taken this flow and turned it into a single command, so whenever I'm ready to make changes I run npm run deploy, and the entire process kicks off.

The code

Most of the heavy lifting is done by gulp-rev-all, which provides an example in the readme. You can read more about things here: https://www.npmjs.com/package/gulp-rev-all

Below is a snippet of code that I setup to get things working for me, it follows the gulp-rev-all example pretty closely.

gulp.task('dist-revision', function() {  
  var refs = [/(.*icons.*)/g, 'index.html'];
  var revAll = new RevAll({
    dontRenameFile: refs,
    dontUpdateReference: refs
  });

  return gulp.src(DIST_DIR + '/**/**')
    .pipe(revAll.revision())
    .pipe(gulp.dest('./build/cdn'));
})

var aws = {  
  'params': {
    'Bucket': 'bucketname'
  },
  'accessKeyId': process.env.AWS_ACCESS_KEY,
  'secretAccessKey': process.env.AWS_SECRET_KEY,
  'distributionId': 'E30r8h0rf8hf',
  'region': 'us-west-2'
};

gulp.task('dist-deploy-static', ['dist', 'dist-revision'], function() {  
  var publisher = awspublish.create(aws);
  var headers = {'Cache-Control': 'max-age=315360000, no-transform, public'};

  return gulp.src(CDN_DIR + '/**/**')
    .pipe(rename(function(path) {
      path.dirname = '/production/' + path.dirname;
    }))
    .pipe(awspublish.gzip())
    .pipe(parallelize(publisher.publish(headers), 50))
    .pipe(publisher.cache())
    .pipe(awspublish.reporter())
    .pipe(cloudfront(aws))
})