Building and Deploying a React App Using AWS Lambda

Posted December 24th, 2017 in cloud-software

If you use React with a static frontend web app, the deployment steps may comprise of the following:

  • Run npm install
  • Run npm test
  • Run npm build
  • Deploy build folder to Amazon S3 & invalidate CDN cache

The first part of the build only requires node.js & npm, and the second part can be written in JavaScript too as a package.json script (I'll go into that more below).

Since node and npm are both available on Lambda, we can write a Lambda function invoked using a post-commit webhook (from GitHub or BitBucket), which builds and deploys the React application.

A few advantages to doing this:

  • You don't need any orchestration infrastructure (Jenkins) or cost overhead of BitBucket pipelines and similar solutions
  • Since the environment is completely managed, the only changes you may need to make are updating the Node.js runtime version as Amazon release newer versions
  • Access to the S3 bucket is controlled using IAM roles, no access key/secret

The Function

This comprises of a simple JavaScript snippet to execute a command and stream the output for logging purposes, and a bash script to run the build.

Function Configuration

  • runtime: Node.js 6.10
  • handler: index.handler
  • memory: 2048
  • timeout: 5 minutes
  • concurrency: 1
  • environment variables: SOURCE_URL (see below), DEPLOY_BUCKET (the name of the S3 bucket to deploy to)

The environment variable SOURCE_URL is a pre-authenticated URL pointing to the latest ZIP of the source control repository - for example GitHub or BitBucket:

https://username:password@github.com/username/repository/archive/branch.zip
https://username:password@bitbucket.org/username/repository/get/branch.zip

Function Code

The entry point index.js (to bootstrap the shell script):

const spawn = require('child_process').spawn;

exports.handler = function(event, context) {
    console.log('Starting build...');
    
    const build = spawn('bash', ['build.sh']);
    
    build.stdout.on('data', function (data) {
      console.log('stdout: ' + data.toString());
    });
    
    build.stderr.on('data', function (data) {
      console.log('stderr: ' + data.toString());
    });
    
    build.on('exit', function (code) {
      console.log('child process exited with code ' + code.toString());
    });
};

The source of build.sh (the bash script running the build):

#!/bin/bash
set -e

# Remove existing source
rm -rf /tmp/*

# Download source
curl -s -o /tmp/source.zip $SOURCE_URL

# Unzip
unzip -q /tmp/source.zip -d /tmp/source

# Move source from the extracted folder up a level
mv /tmp/source/**/* /tmp/source

# Change into source dir
cd /tmp/source

# Set NPM cache to /tmp
export NPM_CONFIG_CACHE=/tmp/npm-cache

# Install deps
npm install --no-optional

# Run unit tests
npm run test

# Production build
npm run build

# Upload (custom script)
npm run upload

Custom NPM Upload Script

To upload the static content to S3, you'll need a custom NPM script executed by npm run upload

"devDependencies": {
    "react-scripts": "1.0.14",
    "s3": "^4.4.0"
},
"scripts": {
    "build": "react-scripts build",
    "upload": "node upload.js"
}

The source of upload.js, to be checked in alongside your packages.json file:

var s3 = require('s3');

var client = s3.createClient();

var uploader = client.uploadDir({
    localDir: 'build',
    s3Params: { Bucket: process.env.DEPLOY_BUCKET },
});

uploader.on('error', function (err) {
    console.log(err);
});

uploader.on('end', function () {
    console.log('Complete');
});

uploader.on('fileUploadStart', function(localFilePath, s3Key) {
    console.log('Uploading ' + localFilePath);
});

uploader.on('fileUploadEnd', function(localFilePath, s3Key) {
    console.log('Uploaded ' + localFilePath);
});

Running the Lambda from a Webhook

In my case, the Lambda takes about 95 seconds to execute. This means that executing the Lambda via API Gateway directly will not work (or at least will not yield a 200 response) as API Gateway times out after 30 seconds.

A better solution is to use API Gateway to post a message to an SNS topic, and subscribe your deployment Lambda to the SNS topic.

You can then configure your chosen provider (GitHub, BitBucket) to post to your endpoint when your respository is pushed to.

Further Work

  • Build at commit - read the commit hash from the WebHook content, pull the source for that commit and build it (trickier without the git command available on Lambda)
  • Cache invalidation - add another custom script to invalidate a CDN's edge caches (such as CloudFront)
  • Environments - part of building as specific commits, but commit to different buckets for dev, staging, production etc
  • Failure notifications - write the build status on the GitHub PR, notify Slack and send an email on failure

Tagged script lambda s3 github bitbucket source commit part node js

Comments

Please click here to load comments.