Hosting NextJS on Cloudfront/S3 is a low maintenance and low cost solution for apps that fit NextJS's static export criteria.
At ArticleAsset, I host part of the testing infrastructure using NextJS static exports with Cloudfront and S3 hosting.
Initially, I was able to deploy the site using this great gist. However, I moved all our infrastructure over to CDK and wanted for find a more CDK-native solution.
Unfortunately, I wasn't able to find any useful guides on how to accomplish that. So after a lot of trial and error, I finally was able to get the deploy pipeline working!
This guide details the steps I took and the roadblocks I encountered to deploy the NextJS static export site on Cloudfront and S3 using the AWS CDK.
Setup DNS and Certificate
First, you'll need a Route53 public hosted zone and a certificate from us-east-1. Regardless of what region your infra is located in, Cloudfront will require a certificate from us-east-1. Thankfully, the CDK makes this a piece of cake!
You'll want to deploy this in 2 steps. In the first step, only deploy the PublicHostedZone construct.
Then go to the Route53 console, grab the hosted zone name servers, and update your domain registrar to point to the hosted zone.
After your registrar updates (can take anywhere from 1 min to 24 hours), then create the certificate.
A quirk I encountered is that if your deploy fails, CDK might not delete the DnsValidatedCertificate during the cleanup phase, so you have to go to the AWS console ACM page and delete it manually before rerunning the deploy. If not, your next deploy will timeout.
Setup NextJS Static Export
We're going to take a quick detour from the CDK and setup our NextJS static export.
Assume our NextJS app is located in the ./web folder.
Update your NextJS build script from next build to next build && next export which is the standard build script for static export.
Also make sure to set compress: false in next.config.js.
We're going to be offloading compression to Cloudfront. NextJS uses gzip compression, whereas Cloudfront uses brotli which has a superior compression ratio.
Setup Cloudfront Function
Cloudfront Functions are functions that you can run at Cloudfront's edge locations. They are not the same as Lambda@Edge. Cloudfront Functions are low latency (runs at edge locations), highly scalable (10,000,000 requests/sec), and very cost effective (free tier + 1/6 the price of lambda).
We need to create a Cloudfront Function to append the .html extension to all requests.
Create a folder called ./functions/fn-format-request
In the above function, we're appending an index.html or .html to the request path so that it matches the object inside S3.
If you've worked with Lambda functions before, you'll notice that we are not export'ing the handler function. This is correct for Cloudfront Functions.
Your package.json should look something like this:
Our build script performs three tasks:
terser --compress --mangle --output build/index.js build/index.js minifies the output. We do this because Cloudfront Functions do not currently work with CDK's NodejsFunction construct. Cloudfront Functions uses its own Function construct, which doesn't have the same functionality as the NodejsFunction construct.
mkdir _next && mv out/_next _next/_next creates a _next folder and moves your cacheable static assets to a separate folder. This is so that we can deploy _next and out folders with different cache-control headers.
Make sure to add _next to your .gitignore
Deploy files to S3
We're creating an S3 Bucket and BucketDeployments to populate the S3 Bucket with our NextJS static export from our local disk.
We have two BucketDeployments:
NoCacheFilesDeployment - Uploads files we don't want cached. This could be *.html files, favicons, robots.txt, or sitemap.xml.
StaticFilesDeployment - Uploads NextJS cachable static assets. This includes our .js and .css files. We enable aggressive caching for these assets since their path is content-hashed.
We're setting prune: false for all the deployments because we want to keep older versions of our site up to handle the case that a user hasn't refreshed their site and is on an older version.
Setup Cloudfront Distribution
Create a Cloudfront Distribution and point it to your S3 Bucket. We're going to setup a default behavior and one additional behavior.
defaultBehavior is used for your .html files and any files in your public folder. We're adding a tiny bit of caching so we can use Cloudfront's compression features (which doesn't work when there's no caching).
/_next/* additional behavior is for the route which corresponds to all of NextJS's static files (ie, .js, .css, .json). We set aggressive caching here as these files never become stale since their filenames are content hashed.
One important detail to get Cloudfront / S3 communication working is to set originRequestPolicy: cf.OriginRequestPolicy.CORS_S3_ORIGIN. If you don't do this, you'll get a 403: Forbidden error.
I believe it's because S3 expects a certain set of headers and if you add any headers that are not expected, it'll deny the request.
FormatRequestFunction is the Cloudfront Function we created in the above step.
We're not setting defaultRootObject because our Cloudfront Function already handles formatting the request uri.
Once you follow these steps, you should be able to deploy your NextJS static export to Cloudfront and S3!
This guide showed you have to deploy your NextJS static export to Cloudfront and S3 using AWS CDK. The steps to do so are
- Create a Route 53 hosted zone and ACM certificate
- Setup NextJS static export
- Setup a Cloudfront Function to transform requests
- Deploy files to S3 using CDK
- Create a Cloudfront Distribution and point it at the S3 bucket using CDK
At ArticleAsset, I'm using this in conjunction with CDK Pipeline, so whenever a PR gets merged, it'll kick off this CDK deployment process. It's been a great low cost and low maintenance solution.