Skip to main content

Docusaurus v3 deployment with AWS S3 and Cloudfront

· 6 min read
Akhan Zhakiyanov
Lead engineer

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.

tip

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:

AdvantagesDisadvantages
Trivial setupBucket name should follow domain name
Custom domain integration with CNAME recordBucket content is public
Index / Error documents support out of the boxContent 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.

note

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).

More details: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html#oac-prerequisites-s3

AdvantagesDisadvantages
Served from multiple locationsBucket name should follow domain name
Custom domain integration via Route53 Alias recordBucket content is public
Index / Error documents support out of the boxCloudfront distribution requires invalidation after docs update
HTTPs support with CloudfrontNo 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/

note

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.

warning
AdvantagesDisadvantages
S3 bucket content is privateIndex / Error documents redirects not supported
Bucket name can be anyCloudfront 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.

tip

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

note

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