├── .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 | 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 | --------------------------------------------------------------------------------