How-To: Setup Gatsby on AWS using GitHub Actions [2/3]: Setup CloudFront Function for index rewrites (with optional basic authentication)
13th April 2022 - (revised version of a post originally published 13th May 2021)
This is Part 2 of a How-To guide to help you host your Gatsby site on AWS (with Continuous Deployment provided by GitHub Actions). See Part 1 for more information.
NOTE: This information should be useful for anyone looking to add Apache style index.html handling to a CloudFront distribution (regardless of whether you're using Gatsby or not)
Out of the box CloudFront has very basic handling of index.html pages. We need to add the following behaviour otherwise some Gatsby generated links won't work:
- When a user requests a page at /my-blog-post (a directory without a trailing /) we need CloudFront to respond with a 301 response to redirect the user's browser to /my-blog-post/ (the same directory with a trailing slash /)
- When a user requests a page at /my-blog-post/ (a directory with a trailing /) we need to serve them the index.html page within that directory: /my-blog-post/index.html.
CloudFront Functions to the rescue
CloudFront Functions allows us to add some logic to CloudFront to modify how CloudFront responds to user requests. We'll a add a small block of code to implement the missing functionality mentioned above.
Note that we could alternatively use Lambda@Edge to set this up - using CloudFront Functions is the new best practice approach (and more cost effective) but if you'd rather use Lambda@Edge please see this older guide
Creating the CloudFront function
- In the AWS console go to the CloudFront section
- In the left hand panel, click
Functions
- Click
Create function
- For Function name enter
gatsby-index
- Cut and paste the following code into the code window (if you also want to add basic Authentication, use the version from the Appendix):
var querystringLib = require('querystring'); function handler(event) { var request = event.request; if (looksLikeADirectory(request.uri) && !request.uri.endsWith('/')) { var redirect = request.uri + '/'; var querystring = stringifyRequestQuerystring(request.querystring); if (querystring) { redirect += '?' + querystring; } return makeRedirect301Response(redirect); } if (request.uri.endsWith('/')) { request.uri += 'index.html'; } return request; } function looksLikeADirectory(path) { var filename = path.split('/').pop(); return !filename.includes('.'); } function makeRedirect301Response(redirect) { return { statusCode: 301, statusDescription: 'Moved Permanently', headers: { location: { value: redirect }, }, }; } function stringifyRequestQuerystring(requestQuerystring) { var stringifableQuerystring = {}; for (var key in requestQuerystring) { if (requestQuerystring[key].multiValue) { stringifableQuerystring[key] = requestQuerystring[key].multiValue.map(mv => mv.value); } else { stringifableQuerystring[key] = requestQuerystring[key].value; } } return querystringLib.stringify(stringifableQuerystring); }
- Click
Save changes
(orange button top-right)
- Click
- Click the
Publish
tab- Click
Publish function
- Click
- In the
Associated distributions
section- Click
Add association
and a popup should appear - Distribution: On the list which appears click the ID for your CloudFront distribution (see Part 1 if you need to determine the ID of your CloudFront distribution)
- Event type: Leave as
Viewer Request
- Cache Behavior: Select
Default (*)
- Click
Add association
- Click
If you're interested to see how the CloudFront Function was linked to the CloudFront distribution you can do the following:
- In the AWS console go to the CloudFront section
- Select the appropriate Distribution in the list
- Click the
Behaviours
tab - Tick the single checkbox in the behaviours table and then click
Edit
- Scroll down and you should see the link to the CloudFront Function
So how does this work?
The code above will be run each time CloudFront tries to fetch a file from the S3 bucket (the Origin of the CloudFront distribution). The code checks the requested url and it either responds with a 301 redirect or it tells CloudFront to fetch the appropriate index.html file from the S3 bucket.
And how much will this cost?
There is a free tier for CloudFront Functions and once that has expired pricing is low.
OK CloudFront function created. What's next?
Next we need to setup the Continuous Deployment environment using GitHub Actions.
Let's move on to Part 3: Continuous Deployment with GitHub Actions.
Appendix - Adding Basic Authentication
If you want to password protect your whole site using Basic Authentication you can use this code instead:
var querystringLib = require('querystring');
var authorizedUsers = [
'user:password',
//... add more entries here as necessary
// all entries should be of the form '<username>:<password>'
];
function handler(event) {
var request = event.request;
if (!isRequestAuthorized(request.headers)) {
return makeNotAuthorizedResponse();
}
if (looksLikeADirectory(request.uri) && !request.uri.endsWith('/')) {
var redirect = request.uri + '/';
var querystring = stringifyRequestQuerystring(request.querystring);
if (querystring) {
redirect += '?' + querystring;
}
return makeRedirect301Response(redirect);
}
if (request.uri.endsWith('/')) {
request.uri += 'index.html';
}
return request;
}
function isRequestAuthorized(requestHeaders) {
if (!requestHeaders.authorization) return false;
if (!requestHeaders.authorization.value.startsWith('Basic ')) return false;
var authValue = requestHeaders.authorization.value.substring(6);
var credentials = String.bytesFrom(authValue, 'base64');
return authorizedUsers.includes(credentials);
}
function makeNotAuthorizedResponse() {
return {
statusCode: 401,
statusDescription: 'Unauthorized',
headers: {
'www-authenticate': { value: 'Basic' },
},
};
}
function looksLikeADirectory(path) {
var filename = path.split('/').pop();
return !filename.includes('.');
}
function makeRedirect301Response(redirect) {
return {
statusCode: 301,
statusDescription: 'Moved Permanently',
headers: {
location: { value: redirect },
},
};
}
function stringifyRequestQuerystring(requestQuerystring) {
var stringifableQuerystring = {};
for (var key in requestQuerystring) {
if (requestQuerystring[key].multiValue) {
stringifableQuerystring[key]
= requestQuerystring[key].multiValue.map(mv => mv.value);
} else {
stringifableQuerystring[key] = requestQuerystring[key].value;
}
}
return querystringLib.stringify(stringifableQuerystring);
}
Don't forget to change the entries in authorizedUsers
for your own username and password. If you need to change the entries later on don't forget to Save and Publish again.
All done? Let's move on to Part 3: Continuous Deployment with GitHub Actions.