CloudFront function to redirect empty document requests to index.html

URL rewrite to append index.html to the URI for single page applications

If you set up an s3 website serving content generated by Hugo or Jekyll, everything works fine, until you decide to use CloudFront to add SSL for example. The website breaks (Access Denied errors) as soon as you try to open the posts. The reason being that when you open https://website.com/blog, you are actually loading https://website.com/blog/index.html. You don’t have to specify that document as the index.html is just default assumption that any web server should be dealing with automatically. Problem is, it doesn’t work with CloudFront (because it is not a web server, neither is s3 and CF talks to the s3 bucket through REST API calls, not HTTP/S). Desperate users open their s3 buckets to the public, set up s3 website endpoint and accellerate that through CloudFront. But there is a better option. Amazon suggests to use a new shiny thing (as usual) - CloudFront functions and specifically url-rewrite-function.

But wait, I can hear you say ‘but there is the default root object setting in CF!’ - yes, there is, but it only applies to the root document (so https://website.com/index.html not website.com/…/index.html) default root object

Deployment steps

Clone the official AWS repo

git clone https://github.com/aws-samples/amazon-cloudfront-functions.git

Create function

cd amazon-cloudfront-functions

aws cloudfront create-function --name url-rewrite-single-page-apps --function-config Comment="Function to redirect empty doc requests to index.html",Runtime=cloudfront-js-1.0 --function-code fileb://url-rewrite-single-page-apps/index.js

If the function was created correctly, the JSON output should look similar to this (make note of the ETag value, you’ll need it in a second!):

{
    "Location": "https://cloudfront.amazonaws.com/2020-05-31/function/arn:aws:cloudfront::<account id>:function/url-rewrite-single-page-apps",
    "ETag": "EXXXXXXXXXXXX",
    "FunctionSummary": {
        "Name": "url-rewrite-single-page-apps",
        "Status": "UNPUBLISHED",
        "FunctionConfig": {
            "Comment": "Function to redirect empty doc requests to index.html",
            "Runtime": "cloudfront-js-1.0"
        },
        "FunctionMetadata": {
            "FunctionARN": "arn:aws:cloudfront::<account id>:function/url-rewrite-single-page-apps",
            "Stage": "DEVELOPMENT",
            "CreatedTime": "2021-12-26T08:43:50.950Z",
            "LastModifiedTime": "2021-12-26T08:43:50.950Z"
        }                     
    }                    
}

Test the function

To validate that the function is working as expected, you can use the JSON test objects in the test-objects directory. To test, use the test-function CLI command as shown in the following example:

$ aws cloudfront test-function --if-match <ETag> --name url-rewrite-single-page-apps --event-object fileb://url-rewrite-single-page-apps/test-objects/file-name-no-extension.json

If the function has been set up correctly, you should see the uri being updated to index.html in the FunctionOutput JSON object:

{
    "TestResult": {
        "FunctionSummary": {
            "Name": "url-rewrite-single-page-apps",
            "Status": "UNPUBLISHED",
            "FunctionConfig": {
                "Comment": "",
                "Runtime": "cloudfront-js-1.0"
            },
            "FunctionMetadata": {
                "FunctionARN": "arn:aws:cloudfront::1234567890:function/url-rewrite-single-page-apps",
                "Stage": "DEVELOPMENT",
                "CreatedTime": "2021-04-09T21:53:20.882000+00:00",
                "LastModifiedTime": "2021-04-09T21:53:21.001000+00:00"
            }
        },
        "ComputeUtilization": "14",
        "FunctionExecutionLogs": [],
        "FunctionErrorMessage": "",
        "FunctionOutput": "{\"request\":{\"headers\":{\"host\":{\"value\":\"www.example.com\"},\"accept\":{\"value\":\"text/html\"}},\"method\":\"GET\",\"querystring\":{\"test\":{\"value\":\"true\"},\"arg\":{\"value\":\"val1\"}},\"uri\":\"/blog/index.html\",\"cookies\":{\"loggedIn\":{\"value\":\"false\"},\"id\":{\"value\":\"CookeIdValue\"}}}}"
    }
}

Publish the function.

Please note that the JSON response states "Status" : "UNPUBLISHED", so the next step is to publish the function.

aws cloudfront publish-function --name url-rewrite-single-page-apps --if-match <ETag>

And - if successful - JSON response should look similar to:

{
    "FunctionSummary": {
        "Name": "url-rewrite-single-page-apps",
        "Status": "UNASSOCIATED",
        "FunctionConfig": {
            "Comment": "Function to redirect empty doc requests to index.html",
            "Runtime": "cloudfront-js-1.0"
        },
        "FunctionMetadata": {
            "FunctionARN": "arn:aws:cloudfront::<account id>:function/url-rewrite-single-page-apps",
            "Stage": "LIVE",
            "CreatedTime": "2021-12-26T08:47:42.111Z",
            "LastModifiedTime": "2021-12-26T08:47:42.111Z"
        }
    }
}

Configuration of the function.

Since it’s created and published, now it needs to be configured.

aws cloudfront get-distribution-config --id <distribution name, NOT ETag!> --output json > dist-cfg.json

Edit the dist-cfg.json:

  • Change the ETag key to IfMatch
  • Modify FunctionAssociation to the following:
"FunctionAssociations": {
                "Quantity": 1,
                "Items" : [     
                        {
                        "EventType" : "viewer-request",
                        "FunctionARN":"arn:aws:cloudfront::<account id>:function/url-rewrite-single-page-apps"
                }
                ]

            },

Modify CloudFront distribution by adding ETag.

Distributions->Distribution ID->Origins->Edit->Add custom header - optional Add ETag, value EXXXXXXXXXXXX

Update the distribution

aws cloudfront update-distribution --id  <CF Distribution ID> --cli-input-json fileb://dist-cfg.json

Other considerations.

Somewhere along the way you should have locked down your s3 bucket so it’s not public, and add a policy (can be done automatically through CloudFront->Distributions->->Edit Origin-> s3 bucket access -> Yes,use OAI (->create a new OAI)-> Yes,update the bucket polcy) to allow for CloudFront access, so the Bucket Policy should look something like:

{
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Sid": "1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity <ID>"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::<bucket name>/*"
        }
    ]
}

(c) Dawid Krysiak https://itisoktoask.me/ http://www.krysiak.biz/