How to Deploy on AWS w/ S3 and Cloudfront?

AWS stands for Amazon Web Services.S3 is their static storage which can be configured for Static Site Hosting.Cloudfront is their CDN (content delivery network)

Hosting a static generated Nuxt app on AWS w/ S3 + Cloudfront is powerful and cheap.

AWS is a death by 1000 paper cuts. If we missed a step, please submit a PR to update this document.

Overview

We'll host super cheap with some AWS services. Briefly:

  • S3
    • cloud data "bucket" for our website files
    • can be configured to host static websites
  • CloudFront
    • a CDN (content delivery network)
    • offers free HTTPS certs
    • Makes your site load faster

We'll push the site like this:

  1. Nuxt Generate -> Local folder -> AWS S3 Bucket -> AWS Cloudfront CDN -> Browser
  2. [ nuxt generate ] [ gulp deploy ]
  3. [ deploy.sh ]

First, we'll generate the site with nuxt generate.Then, we'll use Gulp to publish the files to a S3 bucket and invalidate a CloudFront CDN.

Our deploy script needs these environment variables set:

  • AWS_BUCKET_NAME="example.com"
  • AWS_CLOUDFRONT="UPPERCASE"
  • AWS_ACCESS_KEY_ID="key"
  • AWS_SECRET_ACCESS_KEY="secret"

We'll have these files:

  1. deploy.sh - run `nuxt generate` and `gulp deploy`
  2. gulpfile.js - `gulp deploy` code to push files to S3 and invalidate CloudFront

Setting it up

  • Make a S3 bucket and configure it for static site hosting
  • Create a CloudFront distribution
  • Configure security access
  • Setup build script in your project

AWS: Setup your S3 bucket and CloudFront Distribution

Please follow this tutorial to setup your S3 and CloudFront for step one and two.

You should now have this data:

  • AWS_BUCKET_NAME="example.com"
  • AWS_CLOUDFRONT="UPPERCASE"

AWS: Configure security access

For step 3, we need to create a user that can:

  • Update the bucket contents
  • Invalidate the CloudFront distribution (propagates changes to users faster)

Create a programmatic user with this policy:

NOTE: replace 2x example.com with your S3 bucket name below. This policy allows pushing to the specified bucket, and invalidating any CloudFront distribution.

  1. {
  2. "Version": "2012-10-17",
  3. "Statement": [ {
  4. "Effect": "Allow",
  5. "Action": [ "s3:ListBucket" ],
  6. "Resource": [
  7. "arn:aws:s3:::example.com"
  8. ]
  9. }, {
  10. "Effect": "Allow",
  11. "Action": [
  12. "s3:PutObject",
  13. "s3:PutObjectAcl",
  14. "s3:GetObject",
  15. "s3:GetObjectAcl",
  16. "s3:DeleteObject",
  17. "s3:ListMultipartUploadParts",
  18. "s3:AbortMultipartUpload"
  19. ],
  20. "Resource": [
  21. "arn:aws:s3:::example.com/*"
  22. ]
  23. }, {
  24. "Effect": "Allow",
  25. "Action": [
  26. "cloudfront:CreateInvalidation",
  27. "cloudfront:GetInvalidation",
  28. "cloudfront:ListInvalidations",
  29. "cloudfront:UnknownOperation"
  30. ],
  31. "Resource": "*"
  32. }
  33. ]
  34. }

Then get an access key and secret.

You should now have this data:

  • AWS_ACCESS_KEY_ID="key"
  • AWS_SECRET_ACCESS_KEY="secret"

Laptop: Setup your project's build script

4.1) Create a deploy.sh script. See optional nvm (node version manager).

  1. #!/bin/bash
  2. export AWS_ACCESS_KEY_ID="key"
  3. export AWS_SECRET_ACCESS_KEY="secret"
  4. export AWS_BUCKET_NAME="example.com"
  5. export AWS_CLOUDFRONT="UPPERCASE"
  6. # Load nvm (node version manager), install node (version in .nvmrc), and npm install packages
  7. [ -s "$HOME/.nvm/nvm.sh" ] && source "$HOME/.nvm/nvm.sh" && nvm use
  8. # Npm install if not already.
  9. [ ! -d "node_modules" ] && npm install
  10. npm run generate
  11. gulp deploy

4.2) Make deploy.sh runnable and DON'T CHECK INTO GIT (deploy.sh has secrets in it)

  1. chmod +x deploy.sh
  2. echo "
  3. # Don't commit build files
  4. node_modules
  5. dist
  6. .nuxt
  7. .awspublish
  8. deploy.sh
  9. " >> .gitignore

4.3) Add Gulp to your project and to your command line

  1. npm install --save-dev gulp gulp-awspublish gulp-cloudfront-invalidate-aws-publish concurrent-transform
  2. npm install -g gulp

4.4) Create a gulpfile.js with the build script

  1. const gulp = require('gulp')
  2. const awspublish = require('gulp-awspublish')
  3. const cloudfront = require('gulp-cloudfront-invalidate-aws-publish')
  4. const parallelize = require('concurrent-transform')
  5. // https://docs.aws.amazon.com/cli/latest/userguide/cli-environment.html
  6. const config = {
  7. // Required
  8. params: {
  9. Bucket: process.env.AWS_BUCKET_NAME
  10. },
  11. credentials: {
  12. accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  13. secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  14. signatureVersion: 'v3'
  15. },
  16. // Optional
  17. deleteOldVersions: false, // NOT FOR PRODUCTION
  18. distribution: process.env.AWS_CLOUDFRONT, // CloudFront distribution ID
  19. region: process.env.AWS_DEFAULT_REGION,
  20. headers: { /* 'Cache-Control': 'max-age=315360000, no-transform, public', */ },
  21. // Sensible Defaults - gitignore these Files and Dirs
  22. distDir: 'dist',
  23. indexRootPath: true,
  24. cacheFileName: '.awspublish',
  25. concurrentUploads: 10,
  26. wait: true // wait for CloudFront invalidation to complete (about 30-60 seconds)
  27. }
  28. gulp.task('deploy', function () {
  29. // create a new publisher using S3 options
  30. // http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property
  31. const publisher = awspublish.create(config)
  32. let g = gulp.src('./' + config.distDir + '/**')
  33. // publisher will add Content-Length, Content-Type and headers specified above
  34. // If not specified it will set x-amz-acl to public-read by default
  35. g = g.pipe(parallelize(publisher.publish(config.headers), config.concurrentUploads))
  36. // Invalidate CDN
  37. if (config.distribution) {
  38. console.log('Configured with CloudFront distribution')
  39. g = g.pipe(cloudfront(config))
  40. } else {
  41. console.log('No CloudFront distribution configured - skipping CDN invalidation')
  42. }
  43. // Delete removed files
  44. if (config.deleteOldVersions) { g = g.pipe(publisher.sync()) }
  45. // create a cache file to speed up consecutive uploads
  46. g = g.pipe(publisher.cache())
  47. // print upload updates to console
  48. g = g.pipe(awspublish.reporter())
  49. return g
  50. })

4.5) Deploy and debug

Run it:

  1. ./deploy.sh

You should get an output similar to this:

  1. $ ./deploy.sh
  2. Found '/home/michael/scm/example.com/www/.nvmrc' with version <8>
  3. Now using node v8.11.2 (npm v5.6.0)
  4. > example.com@1.0.0 generate /home/michael/scm/example.com/www
  5. > nuxt generate
  6. nuxt:generate Generating... +0ms
  7. nuxt:build App root: /home/michael/scm/example.com/www +0ms
  8. nuxt:build Generating /home/michael/scm/example.com/www/.nuxt files... +0ms
  9. nuxt:build Generating files... +36ms
  10. nuxt:build Generating routes... +10ms
  11. nuxt:build Building files... +24ms
  12. ████████████████████ 100%
  13. Build completed in 7.009s
  14. DONE Compiled successfully in 7013ms 21:25:22
  15. Hash: 421d017116d2d95dd1e3
  16. Version: webpack 3.12.0
  17. Time: 7013ms
  18. Asset Size Chunks Chunk Names
  19. pages/index.ef923f795c1cecc9a444.js 10.6 kB 0 [emitted] pages/index
  20. layouts/default.87a49937c330bdd31953.js 2.69 kB 1 [emitted] layouts/default
  21. pages/our-values.f60c731d5c3081769fd9.js 3.03 kB 2 [emitted] pages/our-values
  22. pages/join-us.835077c4e6b55ed1bba4.js 1.3 kB 3 [emitted] pages/join-us
  23. pages/how.75f8cb5bc24e38bca3b3.js 2.59 kB 4 [emitted] pages/how
  24. app.6dbffe6ac4383bd30a92.js 202 kB 5 [emitted] app
  25. vendor.134043c361c9ad199c6d.js 6.31 kB 6 [emitted] vendor
  26. manifest.421d017116d2d95dd1e3.js 1.59 kB 7 [emitted] manifest
  27. + 3 hidden assets
  28. Hash: 9fd206f4b4e571e9571f
  29. Version: webpack 3.12.0
  30. Time: 2239ms
  31. Asset Size Chunks Chunk Names
  32. server-bundle.json 306 kB [emitted]
  33. nuxt: Call generate:distRemoved hooks (1) +0ms
  34. nuxt:generate Destination folder cleaned +10s
  35. nuxt: Call generate:distCopied hooks (1) +8ms
  36. nuxt:generate Static & build files copied +7ms
  37. nuxt:render Rendering url /our-values +0ms
  38. nuxt:render Rendering url /how +67ms
  39. nuxt:render Rendering url /join-us +1ms
  40. nuxt:render Rendering url / +0ms
  41. nuxt: Call generate:page hooks (1) +913ms
  42. nuxt: Call generate:page hooks (1) +205ms
  43. nuxt: Call generate:page hooks (1) +329ms
  44. nuxt: Call generate:page hooks (1) +361ms
  45. nuxt:generate Generate file: /our-values/index.html +2s
  46. nuxt:generate Generate file: /how/index.html +0ms
  47. nuxt:generate Generate file: /join-us/index.html +0ms
  48. nuxt:generate Generate file: /index.html +0ms
  49. nuxt:render Rendering url / +2s
  50. nuxt: Call generate:done hooks (1) +4ms
  51. nuxt:generate HTML Files generated in 11.8s +5ms
  52. nuxt:generate Generate done +0ms
  53. [21:25:27] Using gulpfile ~/scm/example.com/www/gulpfile.js
  54. [21:25:27] Starting 'deploy'...
  55. Configured with CloudFront distribution
  56. [21:25:27] [cache] README.md
  57. [21:25:27] [cache] android-chrome-192x192.png
  58. [21:25:27] [cache] android-chrome-512x512.png
  59. [21:25:27] [cache] apple-touch-icon.png
  60. [21:25:27] [cache] browserconfig.xml
  61. [21:25:27] [cache] favicon-16x16.png
  62. [21:25:27] [cache] favicon-32x32.png
  63. [21:25:27] [cache] favicon.ico
  64. [21:25:27] [cache] favicon.svg
  65. [21:25:27] [cache] logo-branches.svg
  66. [21:25:27] [cache] logo-small.svg
  67. [21:25:27] [cache] logo.svg
  68. [21:25:27] [cache] mstile-150x150.png
  69. [21:25:27] [cache] og-image.jpg
  70. [21:25:27] [cache] safari-pinned-tab.svg
  71. [21:25:27] [cache] site.webmanifest
  72. [21:25:28] [create] _nuxt/manifest.421d017116d2d95dd1e3.js
  73. [21:25:29] [update] 200.html
  74. [21:25:30] [create] videos/flag.jpg
  75. [21:25:30] [create] _nuxt/vendor.134043c361c9ad199c6d.js
  76. [21:25:34] [create] videos/flag.mp4
  77. [21:25:34] [cache] _nuxt/pages/how.75f8cb5bc24e38bca3b3.js
  78. [21:25:34] [cache] _nuxt/pages/join-us.835077c4e6b55ed1bba4.js
  79. [21:25:34] [cache] _nuxt/pages/our-values.f60c731d5c3081769fd9.js
  80. [21:25:36] [update] our-values/index.html
  81. [21:25:36] [create] _nuxt/layouts/default.87a49937c330bdd31953.js
  82. [21:25:36] [create] _nuxt/app.6dbffe6ac4383bd30a92.js
  83. [21:25:37] [create] _nuxt/pages/index.ef923f795c1cecc9a444.js
  84. [21:25:38] [update] join-us/index.html
  85. [21:25:38] [update] how/index.html
  86. [21:25:43] [create] videos/flag.webm
  87. [21:25:43] [update] index.html
  88. [21:25:43] CloudFront invalidation created: I16NXXXXX4JDOA
  89. [21:26:09] Finished 'deploy' after 42 s

Note that the CloudFront invalidation created: XXXX is the only output from the CloudFront invalidation npm package. If you don't see that, it's not working.