├── .gitignore
├── README.md
├── circle.yml
├── index.tpl.html
├── package.json
├── scripts
├── build.sh
├── deploy.sh
├── serve.js
└── upload.sh
├── src
├── MyOnlyComponent.jsx
├── cutedog.jpg
├── index.jsx
└── main.less
└── webpack.config.babel.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *sublime-project
3 | *sublime-workspace
4 | dist
5 | npm-debug*
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Super Smooth Deployments with React and AWS
2 |
3 | > Your deploys have never been so pain-free.
4 |
5 | ## About this guide
6 |
7 | This guide will help you register a domain and set it up with Route53, Cloudfront and S3 to have a smooth deployment process.
8 |
9 | ## Overall architecture
10 |
11 | * Webpack creates your **build** in the `/dist/$GIT_HASH` folder. The `$GIT_HASH` here is important, as it means that every resource can be uniquely identified. This means we can indefinitely cache resources without worrying about invalidations.
12 | * We **upload** the contents of the `/dist` folder to `s3://$S3_BUCKET/$ENVIRONMENT`. (i.e. each deploy creates a new folder in your `s3://$S3_BUCKET/$ENVIRONMENT` folder.)
13 | * To **deploy**, we simply copy `index.html` from `s3://$S3_BUCKET/$ENVIRONMENT/$GIT_HASH/` to `s3://$S3_BUCKET/$ENVIRONMENT/`, making sure to specify that this file should never be cached. All resources (CSS, JS, etc.) that you request from this `index.html` file will be requested from their `$GIT_HASH` folders, and can be cached indefinitely.
14 | * Cloudfront will compress these files and serve them from S3.
15 | * Route53 will convert an ugly cloudfront URL to a pretty URL.
16 |
17 | ## Setup the deployment process
18 |
19 | Ok! Let's create the deployment process.
20 |
21 | ### CircleCI
22 |
23 | If you complete the following steps, you should be able to push a new version to the repository.
24 |
25 | * Clone this repository using Github.
26 | * If you do not have a CircleCI account, create one. Then, associate it with your Github account.
27 | * Create an AWS account.
28 | * Go to `https://console.aws.amazon.com/iam/home` and create a user (e.g. `Circle-CI`), to which you should add programmatic access and `AmazonS3FullAccess` permissions.
29 | * Add its Access Key ID and Secret Access Key to CircleCI by going to `Project Settings | AWS Permissions`.
30 | * Decide on a name for your S3 bucket. In build.sh, deploy.sh and upload.sh, change `aws-pres` to the name of your bucket. We will have this `$S3_BUCKET`.
31 |
32 | Now, push your changes to Github and open up CircleCI. You should see your build succeed, and your S3 bucket should have the following structure:
33 |
34 | ```
35 | staging/index.html
36 | staging/$SOME_HASH/index.html
37 | staging/$SOME_HASH/bundle.js
38 | staging/$SOME_HASH/cutedog.png
39 | ```
40 |
41 | * This step is well documented in CircleCI tutorials. If you have trouble, see [https://circleci.com/docs/continuous-deployment-with-amazon-s3/](this document on S3 integration) or [https://circleci.com/docs/getting-started/](this getting started guide).
42 |
43 | ### S3
44 |
45 | * Go to [https://console.aws.amazon.com/s3/home](AWS S3).
46 | * Create a bucket named `$S3_BUCKET`.
47 | * Click on your bucket, click properties, and enable static website hosting. Amazon requires that you need to specify an index document, even though in this case we will not be using it. I put `unused`.
48 | * Take note of the endpoint. It should be similar to `$S3_BUCKET.s3-website-us-east-1.amazonaws.com`.
49 | * You can, optionally, set up a bucket lifecycle policy to delete files after a certain amount of time.
50 |
51 | ### Cloudfront
52 |
53 | * Go to [https://console.aws.amazon.com/cloudfront/home](AWS Cloudfront).
54 | * You'll be creating one cloudfront distribution per environment. For this demo, we'll just worry about setting up a staging cloudfront distribution.
55 | * Create a web distribution with the following settings. Be careful! These settings are intricate and hard to debug.
56 | * **Origin domain name**: from the dropdown, select your S3 bucket
57 | * **Origin path**: Name of the environment, with a slash in front. In this demo, `/staging`
58 | * **Viewer protocol policy**: Redirect HTTP to HTTPS
59 | * **Forward headers**: Whitelist
60 | * **Whitelist headers**: Origin
61 | * **Compress Objects Automatically**: Yes
62 | * **Price class**: I always set the cheapest price class (US, Canada and EU)
63 | * **Alternative Domain Names**: In this example, `staging.$YOUR_DOMAIN`, e.g. in my case, `staging.myawspresentation.com`
64 | * **SSL Certificate**: You will need to go back, after setting up your Route 53 settings, and select the appropriate certificate (`*.$YOUR_DOMAIN`).
65 | * **Default Root Object**: index.html
66 | * Click create! It will take up to an hour for the cloudfront distribution to deploy. You can continue without waiting for it to complete.
67 |
68 | ### Route 53 and Certificate Manager
69 |
70 | * Go to [https://console.aws.amazon.com/route53/home](AWS Route53) and [https://console.aws.amazon.com/acm/home](AWS Certificate Manager).
71 | * You will do the following steps once per environment. In this demo, we'll just worry about staging.
72 | * Register a domain with Route53. It may take Amazon a few minutes to complete your registration, after which you will receive a confirmation email.
73 | * After your Route53 registration is complete, you can request a certificate for `*.$YOUR_DOMAIN`. Use the AWS Certificate Manager to request this. Once this approval completes, you will receive a confirmation email.
74 | * Once you complete this, go back to your Cloudfront distribution, click on your distribution, `Distribution Settings | General | Edit`, and select this certificate.
75 | * Go to Route 53, and click `Create Record Set`.
76 | * **Name**: Fill in the environment name, e.g. `staging`
77 | * **Alias**: Yes
78 | * **Alias target**: Select your cloudfront distribution
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | node:
3 | version: 4.5.0
4 |
5 | dependencies:
6 | cache_directories:
7 | - node_modules
8 | override:
9 | - npm prune && npm config set registry http://registry.npmjs.org/
10 | # Install dependencies
11 | - npm install
12 |
13 | test:
14 | override:
15 | # No tests or linting, this is only a test project!
16 | # - npm run lint
17 | - npm run test
18 |
19 | # First one that matches will be run
20 | deployment:
21 | # # Tags with version numbers get deployed to production
22 | # prod:
23 | # tag: /v[0-9]+(\.[0-9]+)*/
24 | # commands:
25 | # - ./scripts/build.sh production
26 | # - ./scripts/deploy.sh production
27 |
28 | # Branch master is continuously pushed to staging
29 | staging:
30 | branch: master
31 | commands:
32 | - ./scripts/build.sh staging
33 | - ./scripts/upload.sh staging
34 | - ./scripts/deploy.sh staging
35 |
36 | # Every other branch is prepared to be deployed to sandbox, but not deployed
37 | sandbox:
38 | branch: /.+/
39 | commands:
40 | - ./scripts/build.sh sandbox
41 | - ./scripts/upload.sh sandbox
42 |
--------------------------------------------------------------------------------
/index.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | AWS Presentation
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aws-pres",
3 | "babel": {
4 | "presets": [
5 | "es2015",
6 | "stage-0",
7 | "react"
8 | ]
9 | },
10 | "dependencies": {
11 | "react": "^15.4.2",
12 | "react-dom": "^15.4.2"
13 | },
14 | "devDependencies": {
15 | "babel-core": "^6.21.0",
16 | "babel-loader": "^6.2.10",
17 | "babel-polyfill": "^6.20.0",
18 | "babel-preset-es2015": "^6.18.0",
19 | "babel-preset-react": "^6.16.0",
20 | "babel-preset-stage-0": "^6.16.0",
21 | "css-loader": "^0.26.1",
22 | "file-loader": "^0.9.0",
23 | "html-webpack-plugin": "^2.26.0",
24 | "less": "^2.7.2",
25 | "less-loader": "^2.2.3",
26 | "react-hot-loader": "^1.3.1",
27 | "style-loader": "^0.13.1",
28 | "webpack": "^1.14.0",
29 | "webpack-dev-server": "^1.16.2"
30 | },
31 | "scripts": {
32 | "start": "babel-node ./scripts/serve.js",
33 | "clean": "rm -rf ./dist",
34 | "build": "npm run clean && webpack --bail",
35 | "test": "echo Testing Testing 1, 2, 3. Okay, you pass."
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This file uploads the dist folder to S3 and iff in staging or production, sets it as active
4 |
5 | if [ -z $CIRCLECI ]; then
6 | echo "This script should run on CircleCI only. Bailing..."
7 | exit 1
8 | fi
9 |
10 | export NODE_ENV=production
11 | S3_BUCKET=aws-pres
12 | S3_BUCKET_FOLDER=$1
13 |
14 | if [ -z $S3_BUCKET_FOLDER ]; then
15 | echo "***** S3_BUCKET_FOLDER is required. Bailing..."
16 | exit 1
17 | fi
18 |
19 |
20 | echo "***** About to build with environment $NODE_ENV"
21 | npm run build
22 |
23 | if [ $? -ne 0 ]; then
24 | echo "***** Failed building for environment $NODE_ENV to $S3_BUCKET_FOLDER"
25 | exit 1
26 | fi
27 |
28 | echo "***** Succeeding building"
--------------------------------------------------------------------------------
/scripts/deploy.sh:
--------------------------------------------------------------------------------
1 | DIST_HASH=${2:-$CIRCLE_SHA1}
2 |
3 | if [ -z $DIST_HASH ]; then
4 | echo "***** A hash to deploy is needed. Bailing..."
5 | exit 1
6 | fi
7 |
8 | S3_BUCKET=aws-pres
9 | S3_BUCKET_FOLDER=$1
10 |
11 | if [ -z $S3_BUCKET_FOLDER ]; then
12 | echo "***** S3_BUCKET_FOLDER is required. Bailing..."
13 | exit 1
14 | fi
15 |
16 | aws s3 cp \
17 | s3://$S3_BUCKET/$S3_BUCKET_FOLDER/$DIST_HASH/index.html \
18 | s3://$S3_BUCKET/$S3_BUCKET_FOLDER/index.html \
19 | --acl public-read \
20 | --cache-control max-age=0,no-cache \
21 | --metadata-directive REPLACE
22 |
23 | if [ $? -ne 0 ]; then
24 | echo "***** Failed setting build $DIST_HASH build as active"
25 | exit 1
26 | fi
27 |
28 | echo "***** Succeeded activating build $DIST_HASH for $NODE_ENV in $S3_BUCKET_FOLDER"
--------------------------------------------------------------------------------
/scripts/serve.js:
--------------------------------------------------------------------------------
1 | import WebpackDevServer from 'webpack-dev-server';
2 | import webpack from 'webpack';
3 | import webpackConfig from '../webpack.config.babel';
4 |
5 | const compiler = webpack(webpackConfig);
6 |
7 | const server = new WebpackDevServer(compiler, {
8 | publicPath: '/',
9 | watchOptions: {
10 | aggregateTimeout: 300,
11 | poll: 1000,
12 | },
13 | stats: 'minimal',
14 | hot: true,
15 | });
16 |
17 | server.listen(3000);
18 |
--------------------------------------------------------------------------------
/scripts/upload.sh:
--------------------------------------------------------------------------------
1 | if [ -z $CIRCLECI ]; then
2 | echo "This script should run on CircleCI only. Bailing..."
3 | exit 1
4 | fi
5 |
6 | S3_BUCKET=aws-pres
7 | S3_BUCKET_FOLDER=$1
8 |
9 | if [ -z $1 ]; then
10 | S3_BUCKET_FOLDER=$NODE_ENV
11 | fi
12 |
13 | echo "***** About to deploy build to $S3_BUCKET/$S3_BUCKET_FOLDER"
14 |
15 | # copy all of the files to aws
16 | aws s3 cp ./dist/ s3://$S3_BUCKET/$S3_BUCKET_FOLDER \
17 | --acl public-read \
18 | --recursive
19 |
20 | if [ $? -ne 0 ]; then
21 | echo "***** Failed uploading build to $S3_BUCKET/$S3_BUCKET_FOLDER"
22 | exit 1
23 | fi
24 |
25 | echo "***** Succeeded uploading build to $S3_BUCKET/$S3_BUCKET_FOLDER"
--------------------------------------------------------------------------------
/src/MyOnlyComponent.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import './main.less';
4 | import cuteDogUrl from './cutedog.jpg';
5 |
6 | export default class MyOnlyComponent extends Component {
7 | render() {
8 | return (
9 |
Super Smooth Deployments with React and AWS
10 |
Hello from NYC HTML5 BYAH!
11 |
12 | - This is the demo site that accompanies the presentation "Super Smooth Deployments with React and AWS".
13 | -
14 | The source code can be found
15 | here.
16 |
17 | - Look at README.md, webpack.config.babel.js, package.json and the scripts/ folder.
18 | - Hey! Look at this cute dog!
19 |
20 |
21 |
22 |
);
23 | }
24 | }
--------------------------------------------------------------------------------
/src/cutedog.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbalicki2/react-aws-deployments/a53b4279c811fffc3e004a8ff6d32d101647671a/src/cutedog.jpg
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 |
4 | import MyOnlyComponent from './MyOnlyComponent.jsx';
5 |
6 | render(,
7 | document.getElementById('app'));
8 |
--------------------------------------------------------------------------------
/src/main.less:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #FFF9F9;
3 | }
--------------------------------------------------------------------------------
/webpack.config.babel.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import HtmlWebpackPlugin from 'html-webpack-plugin';
3 | import webpack from 'webpack';
4 | import childProcess from 'child_process';
5 |
6 | const isProduction = (process.env.NODE_ENV === 'production');
7 |
8 | const getGitRev = () => process.env.CIRCLE_SHA1
9 | || childProcess.execSync('git rev-parse HEAD').toString().trim();
10 |
11 | const staticFolder = isProduction
12 | ? getGitRev()
13 | : 'static';
14 |
15 | // locally, we cannot have a leading slash. In production, though,
16 | // we want one, because otherwise the request will not be relative to the root.
17 | const staticFolderWithSlash = `${isProduction ? '/' : ''}${staticFolder}`;
18 |
19 | export default {
20 | entry: {
21 | [`${staticFolderWithSlash}/bundle.js`]: [
22 | './src/index',
23 | ].concat(
24 | isProduction
25 | ? []
26 | : [
27 | 'webpack/hot/only-dev-server',
28 | 'webpack-dev-server/client?http://localhost:3000',
29 | ]
30 | ),
31 | },
32 | resolve: {
33 | extensions: ['', '.js', '.jsx'],
34 | },
35 | output: {
36 | filename: '[name]',
37 | path: path.resolve('dist'),
38 | publicPath: '',
39 | },
40 | devtool: isProduction ? undefined : '#cheap-module-eval-source-map',
41 | module: {
42 | loaders: [
43 |
44 | // JS
45 | {
46 | test: /\.jsx?$/,
47 | exclude: /node_modules/,
48 | loaders: (isProduction ? [] : ['react-hot'])
49 | .concat(['babel']),
50 | },
51 |
52 | // LESS
53 | { test: /\.less$/, loader: 'style!css!less' },
54 |
55 | // images
56 | { test: /\.(jpeg|png|jpg)$/, loader: `file?name=${staticFolderWithSlash}/[name].[ext]` },
57 | ],
58 | },
59 | plugins: [
60 | new HtmlWebpackPlugin({
61 | template: 'index.tpl.html',
62 | filename: path.join(isProduction ? staticFolder : '', 'index.html'),
63 | }),
64 | ].concat(isProduction
65 | ? []
66 | : [new webpack.HotModuleReplacementPlugin()]
67 | ),
68 | };
69 |
--------------------------------------------------------------------------------