The Goal
We plan to host NextJs frontend assets on S3 bucket, using NextJS static export, which will generate a HTML file per route, such as index.html, portfolio.html etc. This is to reduce first load bundle size.
NextJs static export is achieved with this:
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
output: 'export'
}
module.exports = nextConfig
The Problem
Discovered routing issue when trying to navigate to pages directly.
- For direct routes like "
/portfolios
", we need to append a ".html
" to the url in order to get to the page and it is not ideal. - For nextjs dynamic routes such as "
/portfolios/[portfolio_id]
", it is a bigger issue, as we do not expect user to search with "https://www.coolweb/portfolios/[portfolio_id].html
" and it does not go to the desiredportfolio_id
anyway.
The Solution
Cloudfront with Lambda@Edge
We will create a Lambda@Edge that will have logic to resolve the routing issue and then we associate this Lambda to the origin request of the CloudFront Distribution.
Origin request refers to a request made by CloudFront to the origin server for content that is not currently cached or needs to be refreshed in the cache. In our case the origin server is the S3 bucket where we upload nextjs static assets.
The association will look like this in the aws console.
Sample Yaml file that creates the Lambda Edge Function and the CloudFront Distribution.
AWSTemplateFormatVersion: '2010-09-09'
Description: Lambda Edge Function - Rewrite Nextjs Static Export Routing
Parameters:
ProjectName:
Description: The Project name
Type: String
Default: awesome
LambdaRole:
Type: String
Description: Lambda IAM Role Arn
LambdaEdgeName:
Type: String
Description: "Lambda Edge Name"
Default: "router-lambda-edge"
Resources:
LambdaEdgeRedirectFunction:
Type: AWS::Lambda::Function
Properties:
Description: Lambda to rewrite Nextjs Static Export Routing
Runtime: nodejs18.x
# lambda edge does not support arm64 yet
Architectures:
- x86_64
Role: !Ref LambdaRole
FunctionName: !Sub "${ProjectName}-${LambdaEdgeName}"
Handler: index.handler
Code:
ZipFile: |
// check if url has extension like .html
const hasExtension = /(.+)\.[a-zA-Z0-9]{2,5}$/;
// check if url end with '/'
const hasSlash = /\/$/;
// check for dynamic routes
const dynamicPortfolioRoutes = /\/portfolios\/\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b/
exports.handler = async(event,context, callback) => {
const { request } = event.Records[0].cf;
console.log('req',request)
const url = request.uri
if (url){
// check for dynamic portfolios route
if (url.match(dynamicPortfolioRoutes) && !url.match(hasExtension) && !url.match(hasSlash)) {
request.uri = `${url.substr(0, url.lastIndexOf('/'))}/[portfolio_id].html`;
return callback(null, request);
}
// check for fixed route
if (!url.match(hasExtension) && !url.match(hasSlash)) {
request.uri = `${url}.html`;
return callback(null, request);
}
}
// If nothing matches, return request unchanged
return callback(null, request);
}
LambdaEdgeRedirectFunctionVersion:
Type: AWS::Lambda::Version
Properties:
FunctionName: !GetAtt LambdaEdgeRedirectFunction.Arn
CloudFrontOriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: S3 Origin Access Identity
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
CustomErrorResponses:
- ErrorCachingMinTTL: 10
ErrorCode: 403
ResponseCode: 403
ResponsePagePath: "/index.html"
- ErrorCachingMinTTL: 10
ErrorCode: 404
ResponseCode: 404
ResponsePagePath: "/index.html"
DefaultCacheBehavior:
TargetOriginId: bucket
DefaultTTL: 86400
Compress: true
ForwardedValues:
QueryString: false
ViewerProtocolPolicy: redirect-to-https
PriceClass: "PriceClass_All"
LambdaFunctionAssociations: # associate lambda edge at us-east-1 for nextjs fe routing rewrite
- EventType: origin-request
LambdaFunctionARN: !Join
- ':'
- - !GetAtt [LambdaEdgeRedirectFunction, Arn]
- !GetAtt [LambdaEdgeRedirectFunctionVersion, Version]
DefaultRootObject: "index.html"
Enabled: true
HttpVersion: http2
Origins:
- Id: bucket
DomainName: !Sub: "${BucketName}.s3-${AWS::Region}.amazonaws.com"
S3OriginConfig:
OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}"
The above yaml sample will not work out of the box, you need to also create the following resources:
- Lambda Role should have the following Trust Relationship:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
- edgelambda.amazonaws.com
Action: sts:AssumeRole
- The Lambda Role should also allowed lambda invocations and s3 read access:
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- lambda:InvokeFunction
- lambda:InvokeAsync
Resource:
- !Sub "arn:aws:lambda: ..."
- Effect: Allow
Action:
- S3:Get*
- S3:List*
Resource:
- !Sub "arn:aws:s3: ..."
- Create a S3 resource with Bucket Policy that allows
s3:GetObject
for the Cloudfront distribution using an Origin Access Identity (OAI):
MyS3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: <your-s3-bucket>
AccessControl: Private
BucketPolicy:
Version: "2012-10-17"
Statement:
- Sid: AllowCloudFrontAccess
Effect: Allow
Principal:
CanonicalUser: !GetAtt MyCloudFrontOriginAccessIdentity.S3CanonicalUserId
Action: s3:GetObject
Resource: !Sub "arn:aws:s3:::${MyS3Bucket}/*"
Thanks for Reading!
Top comments (0)