Docusaurus v3 deployment with AWS S3 and Cloudfront
Docusaurus v3 is out, but official docs are still lacking deployment to AWS as option.
Let me fix this and guide you how to create most secure and up to date approach with AWS S3 and Cloudfront.
TL;DR final approach:
- private S3 bucket without website static hosting enabled
- Cloudfront distribution with Origin Access Control (OAC)
- Cloudfront Function to handle redirects to index.html
You can skip development instructions and go to full samples source code available at https://github.com/ahanoff/how-to/tree/main/docusaurus-3-deployment-with-s3-and-cloudfront
Docusaurus is static website generator, so let's review options AWS offers us to serve it.
Serving static websites on AWS
Use S3 bucket static website hosting feature
After S3 bucket creation you can enable static website feature for it. While being most trivial to setup it has few disadvantages that might be critical:
Advantages | Disadvantages |
---|---|
Trivial setup | Bucket name should follow domain name |
Custom domain integration with CNAME record | Bucket content is public |
Index / Error documents support out of the box | Content served from single region |
No HTTPS support for website endpoint |
Use S3 static website endpoint as Cloudfront custom origin
Similar to previous setup, but now uses Cloudfront with custom origin. Unfortunately neither OAC nor OAI supported.
If your origin is an Amazon S3 bucket configured as a website endpoint, you must set it up with CloudFront as a custom origin. That means you can't use OAC (or OAI).
Advantages | Disadvantages |
---|---|
Served from multiple locations | Bucket name should follow domain name |
Custom domain integration via Route53 Alias record | Bucket content is public |
Index / Error documents support out of the box | Cloudfront distribution requires invalidation after docs update |
HTTPs support with Cloudfront | No OAC support |
Only HTTP traffic supported between Cloudfront and S3 website |
Use S3 origin with Cloudfront Origin Access Control (OAC)
Amazon CloudFront Origin Access Control is a new feature that enables CloudFront customers to easily secure their S3 origins by permitting only designated CloudFront distributions to access their S3 buckets.
More details at https://aws.amazon.com/blogs/networking-and-content-delivery/amazon-cloudfront-introduces-origin-access-control-oac/
When you use CloudFront OAC with Amazon S3 bucket origins, you must set Amazon S3 Object Ownership to Bucket owner enforced, the default for new Amazon S3 buckets. If you require ACLs, use the Bucket owner preferred setting to maintain control over objects uploaded via CloudFront.
Origin Access Identity (OAI) is legacy and won't be considered
Advantages | Disadvantages |
---|---|
S3 bucket content is private | Index / Error documents redirects not supported |
Bucket name can be any | Cloudfront distribution requires invalidation after docs update |
Content served from multiple locations | |
HTTPs support with Cloudfront | |
Custom domain integration |
Distribution invalidation is inevitable, thus the only remaining issue is request redirects: Docusaurus is React single page application, thus it's important to have redirection to index.html
file.
Luckily these redirects can be solved with Cloudfront functions
Development
In this section, I’ll guide you through the development process, demonstrating the creation of an Docusaurus application, cover how to deploy it with an AWS CLI.
For your convenience, I’ll provide Pulumi code snippets written in Typescript — for creating an S3 bucket, Origin Access Control (OAC), Cloudfront function, Cloudfront distribution and S3 bucket policy.
You can skip development instructions and go to full source code available at https://github.com/ahanoff/how-to/tree/main/docusaurus-3-deployment-with-s3-and-cloudfront
Infrastructure as Code
First we create S3 bucket to store Docusaurus build output files
import * as aws from "@pulumi/aws";
const docusaurusBucket = new aws.s3.Bucket("docusaurus-3-bucket", {
bucket: 'docusaurus-3'
});
Create Cloudfront OAC. OAC will sign requests in order to access S3 bucket content
const oac = new aws.cloudfront.OriginAccessControl('docusaurus-3-cloudfront-oac', {
originAccessControlOriginType: 's3',
signingBehavior: 'always',
signingProtocol: 'sigv4',
description: 'OAC to allow Cloudfront access to S3',
name: 'docusaurus-3-cloudfront-oac'
})
Create js function that redirects requests to index.html
'use strict';
function handler(event) {
var request = event.request;
var uri = request.uri;
// Check whether the URI is missing a file name.
if (uri.endsWith('/')) {
request.uri += 'index.html';
}
// Check whether the URI is missing a file extension.
else if (!uri.includes('.')) {
request.uri += '/index.html';
}
return request;
}
Create Cloudfront function with code from previous step
import * as fs from "fs";
const cloudfrontFunction = new aws.cloudfront.Function('docusaurus-3-redirect-to-index', {
runtime: 'cloudfront-js-2.0',
name: 'docusaurus-3-redirect-to-index',
code: fs.readFileSync(`redirect-function.js`, "utf8"),
publish: true
})
Now we create Cloudfront Distribution with S3 origin and Cloud function used in default cache behavior:
Cloudfront distribution
const distribution = new aws.cloudfront.Distribution('docusaurus-3-cloudfront-distribution', {
enabled: true,
restrictions: {
geoRestriction: {
restrictionType: 'none'
}
},
origins: [
{
domainName: docusaurusBucket.bucketRegionalDomainName,
originId: docusaurusBucket.id,
originAccessControlId: oac.id,
}
],
defaultCacheBehavior: {
targetOriginId: docusaurusBucket.id,
viewerProtocolPolicy: "redirect-to-https",
allowedMethods: ["GET", "HEAD", "OPTIONS"],
cachedMethods: ["GET", "HEAD", "OPTIONS"],
forwardedValues: {
queryString: false,
cookies: { forward: "none" },
},
functionAssociations: [
{
eventType: 'viewer-request',
functionArn: interpolate`${cloudfrontFunction.arn}`
}
],
minTtl: 0,
defaultTtl: 86400,
maxTtl: 31536000,
},
viewerCertificate: {
cloudfrontDefaultCertificate: true,
},
})
Create S3 bucket policy to allow Cloudfront distribution read bucket content
Bucket policy can not reference non existing resource ARNs, thus distribution should exist before bucket policy creation.
In Pulumi code we use dependsOn
Bucket policy
new aws.s3.BucketPolicy('docusaurus-3-bucket-policy', {
bucket: docusaurusBucket.bucket,
policy: {
Version: '2008-10-17',
Statement: [
{
Effect: 'Allow',
Principal: aws.iam.Principals.CloudfrontPrincipal,
Action: 's3:GetObject',
Resource: interpolate`${docusaurusBucket.arn}/*`,
Condition: {
StringEquals: {
'AWS:SourceArn': interpolate`${distribution.arn}`
}
}
}
]
}
}, {
/**
* Distribution needs to be created before it can be referenced in bucket policy. Otherwise you get the error below:
* putting S3 Bucket (docusaurus-3) Policy: operation error S3: PutBucketPolicy, https response error StatusCode: 400, RequestID: , HostID: , api error MalformedPolicy: Policy has invalid resource
*/
dependsOn: distribution
})
Run pulumi up
and wait until resources provisioning completed
Create Docusaurus app
Create default Docusaurus 3 application with classic template:
npm init docusaurus docusaurus3 classic
npm run build
After Docusaurus build output is ready we can upload it to S3 bucket.
aws s3 sync ./docusaurus3/build s3://docusaurus-3
To clear CDN cache we need to create distribution invalidation
aws cloudfront create-invalidation --distribution-id EDFDVBD6EXAMPLE --paths '/*'
Summary
While AWS offers multiple options to host your static websites, in this article we cover how to implement most secure and up to date approach for it using Docusaurus as example
All codebase is available at https://github.com/ahanoff/how-to/tree/main/docusaurus-3-deployment-with-s3-and-cloudfront