Photo by Krista Bennett on Unsplash
Solving CloudFront Path Routing Issues for Static Websites Hosted on S3
Introduction
Recently, at work, I encountered the following scenario:
A static website was uploaded to an S3 bucket with a filepath similar to the following:
- index.html
- page1
- index.html
- page2
- index.html
This S3 bucket file was served through CloudFront.
The use case was simple:
Let’s assume the CloudFront URL was www.just-an-example.xyz,
So,
Visiting www.just-an-example.xyz should serve the root index.html, Going to www.just-an-example.xyz/page1 should serve the page1/index.html.
The problem arose when www.just-an-example.xyz/page1/index.html was working fine, but www.just-an-example.xyz/page1 wasn’t.
It sounds simple, but the solution wasn’t as straightforward as I thought.
This post provides the solution to the above issue. It’s written for someone else who might face this problem and for my future self, who will no doubt forget about this and find myself scratching my head in the future. This assumes you are familiar with AWS services such as buckets, IAM policies, and CloudFront.
Step 1: Enable Static Website Hosting for S3 Bucket
Navigate to the S3 bucket properties page and ensure static website hosting is enabled if not done already.
Step 2: Configure Bucket Permissions
Set up the bucket policy with the following permissions:
{
"Version": "2008-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::<YOUR_BUCKET_NAME>/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::<ACCOUNT_NUMBER>:distribution/<CLOUDFRONT_DISTRIBUTION_ID>"
}
}
},
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::<YOUR_BUCKET_NAME>/*"
}
]
}
Also, enable public access to the bucket.
Step 3: Create Lambda Function
'use strict';
exports.handler = (event, context, callback) => {
let request = event.Records[0].cf.request;
let olduri = request.uri;
if (olduri.indexOf(".") === -1) {
olduri = olduri + '\/';
}
olduri = olduri.replace(/\/\//, '\/');
request.uri = olduri.replace(/\/$/, '\/index.html');
request.headers['host'] = [{ key: 'host', value: '<###>-east-1.amazonaws.com'}];
// Return to CloudFront
return callback(null, request);
};
We are basically telling any request to www.just-an-example.xyz/page1 should serve www.just-an-example.xyz/page1/index.html
Replace <###>
with your S3 bucket's static URL. Deploy the Lambda function and publish a new version.
Step 4: Update Lambda Role Permissions
In the Lambda configuration tab, under permissions, click on the role name link.
Update the trust relationships to match the following:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"lambda.amazonaws.com",
"edgelambda.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
}
Step 5: Update CloudFront Configuration
Change the origin to point to the static S3 bucket URL created above from the origins tab.
Update the behavior config in the behavior tab:
For cache settings, choose legacy cache settings and include the host header. Reference the image below for clarity.
Finally, in the function associations, update the origin request to point to the previous Lambda function ARN along with the version number.
Finally, invalidate the files if needed.
Phew, with these steps, the issue should be resolved.