Solving CloudFront Path Routing Issues for Static Websites Hosted on S3

Published at Nov 21, 2023

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.

Enabling static website for s3 bucket

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.

Finding lambda function's role

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.

Cache settings in the behavior tab of cloudfront distribution

Finally, in the function associations, update the origin request to point to the previous Lambda function ARN along with the version number.

Associating lambda function with the cloudfront distribution

Finally, invalidate the files if needed.

Phew, with these steps, the issue should be resolved.

Arjan Dhakal © 2024