301 redirects for files hosted in Amazon S3 using Lambda@Edge Functions

The pur­pose of this guide is to walk you through the step-by-step process of cre­at­ing 301 redi­rects for files host­ed in Ama­zon S3 using Lambda@Edge functions.

For more infor­ma­tion on the dif­fer­ences between Cloud­Front func­tions and Lambda@Edge Func­tions, please vis­it this link.

Cre­ate the func­tion #

  1. Go to your AWS dash­board and search for Lamb­da. Once there, on the top right cor­ner click on Create Function button
  2. For our case study, we are going to choose Node.js, but you have oth­er options like .NET, Python, Java, Ruby
  3. Leave the rest as it is and click Create Function.

Lambda@Edge Base Code This base code will stop serv­ing the final asset and will instead return an OK screen.

Han­dling Requests #

The func­tion receives an event, and with­in this event lies the request data. The pro­vid­ed code offers 3 sam­ples on how to han­dle the requests:

  1. If the request URI con­tains /assets/old-slug/ redi­rect to /assets/new-slug/
  2. If the request URI con­tains one of the URLs pro­vid­ed in a JSON vari­able, redi­rect to anoth­er asset.
  3. If the request URI ends with a known string, redi­rect by replac­ing the end­ing of the asset’s name.

Redi­rect­ing an asset is not lim­it­ed to these 3 sam­ples, you have the flex­i­bil­i­ty to redi­rect to alter­na­tive buck­ets or domains, mod­i­fy asset head­er respons­es, imple­ment cache key nor­mal­iza­tion, han­dle HTTP to HTTPS requests, and explore var­i­ous oth­er cus­tomiza­tion options. Giv­en that the code is writ­ten in pure JavaScript, you have the free­dom to use inno­v­a­tive approach­es to achieve your desired solutions.

import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);

const brokenUrlsObj = require('./routes.json');

export const handler = async (event) => {
  const domain = 'https://myS3bucketDomain.com';
  const currentFolderUri = '/assets/old-slug/';
  const newFolderUri = '/assets/new-slug/';
  const request = event.Records[0].cf.request;
  var newUri = request.uri;
  
  console.log(request)

  var brokenUrls = brokenUrlsObj[0];
  
  if(request.uri.includes(currentFolderUri)){
    //change the uri
    newUri = request.uri.replace(currentFolderUri, newFolderUri)
    return {
        status: '301',
        statusDescription: 'Moved Permanently',
        headers: {
            location: [{ value: domain + newUri }]
        }
    };
  }
  
  //fully change a filename
 if (brokenUrls[request.uri] !== undefined) {
    return {
        status: '301',
        statusDescription: 'Moved Permanently',
        headers: {
            location: [{ value: domain + brokenUrls[request.uri] }]
        }
    };
  }
  
  //change part of the filename
  else if (request.uri.endsWith('-categoryA.pdf')) {
    newUri = request.uri.replace('-categoryA.pdf', '-categoryB.pdf')
    return {
        status: '301',
        statusDescription: 'Moved Permanently',
        headers: {
            location: [{ value: domain + newUri }]
        }
    };
  }
  
  return request;
};

From lines 15 to 27, it redi­rects any request that con­tains a URI with /assets/old-slug/ to /assets/new-slug/

From lines 30 to 38, it redi­rects any request that match­es any URI in the JSON file, brokenUrls, that was import­ed at the begin­ning of code, to its new URL.

From lines 40 to 50, it redi­rects any request that ends with -categoryA.pdf to a new URL for it, end­ing with -categoryB.pdf.

Last­ly, line 52 return request; will allow all the oth­er requests to keep going with their nor­mal route.

Please take note of the slight vari­a­tion in cap­tur­ing the request com­pared to how it is accom­plished with Cloud­Front Functions.

const request = event.Records[0].cf.request;

The test­ing JSON file for this func­tion is:

[
   {
      "/assets/folder1/yourFilename1.pdf":"/assets/new-slug/yourFilename1.pdf",
      "/assets/folder1/yourFilename2.pdf":"/assets/new-slug/yourFilename2.pdf",
      "/assets/folder1/yourFilename3.pdf":"/assets/new-slug/yourFilename3.pdf",
      "/assets/folder1/yourFilename4.pdf":"/assets/new-slug/yourFilename4.pdf",
      "/assets/temp-folder/yourFilenameA.pdf":"/assets/new-slug/yourFilenameA.pdf",
      "/assets/temp-folder/yourFilenameB.pdf":"/assets/new-slug/yourFilenameB.pdf",
      "/assets/temp-folder/yourFilenameC.pdf":"/assets/new-slug/yourFilenameC.pdf",
      "/assets/temp-folder/yourFilenameD.pdf":"/assets/new-slug/yourFilenameD.pdf"
   }
]

How to test your code. #

Since the lan­guage is Javascript, you can use console.log() to print and debug any variable.

For exam­ple: consoel.log(request);

Once you have your code in place, click on Deploy, which is the equiv­a­lent to Save (don’t wor­ry, is not going to put the func­tion live just yet), then click on the blue but­ton Test. (If you don’t hit Deploy, you won’t see the changes on your tests). 

For Test Event Action, leave it as Create new event, write a name for your test, and then select CloudFront A/B Test under the Template select, it will give you a default tem­plate to sim­u­late a request. 

On the Event JSON find URI and write one of your pos­si­ble cas­es on the field URL Path, for exam­ple /assets/old-slug/testing.pdf, and click Save. This test will be reusable. With Lambda@Edge, you can cre­ate and save dif­fer­ent tests with dif­fer­ent scenarios.

Lambda@Edge Base Test

After sav­ing the test, it will take you back to your main code, click the test but­ton and you will see the response to your test request.

Lambda@Edge Base Test Result

A new tab named Execution Result will con­tain the obtained response, in this case, it was a redi­rect from myS3bucketDomain.com/assets/old-slug/yourFilename1.pdf to myS3bucketDomain.com/assets/new-slug/yourFilename1.pdf.

If your code includes a console.log() state­ment, the cor­re­spond­ing out­put will be vis­i­ble under Function Logs next to the word INFO, the request vari­able in my case. (See last image for reference).

IAM per­mis­sions #

To con­fig­ure Lambda@Edge, you must set up spe­cif­ic IAM per­mis­sions and an IAM exe­cu­tion role. Lambda@Edge also cre­ates ser­vice-linked roles to repli­cate Lamb­da func­tions to Cloud­Front Regions and to enable Cloud­Watch to use Cloud­Front log files.”

Steps:

  1. While in the func­tion, click the tab Configuration
  2. On the right Side­bar click Permissions
  3. Click the role name dis­play under Role name, this is the role for your func­tion and it will take you to the Iden­ti­ty and Access Man­age­ment (IAM)

Lambda@Edge Role

  1. While in the IAM Dash­board, click the tab Trust relationships
  2. Click Edit trust policy
  3. Add the line "edgelambda.amazonaws.com" under ‘“Ser­vice”’,
  4. Click Update Policy.

Your code will look like this:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": [
                    "edgelambda.amazonaws.com",
                    "lambda.amazonaws.com"
                ]
            },
            "Action": "sts:AssumeRole"
        }
    ]
		}

Cre­ate a Behav­ior for your Cloud­Front dis­tri­b­u­tion #

To be able to exe­cute the func­tion, you need to cre­ate a behav­ior on your Cloud­Front dis­tri­b­u­tion. This will trig­ger the func­tion under cer­tain circumstances.

For our case sce­nario, we are going to cre­ate a behav­ior that it will trig­ger the func­tion if the request URI ends with the exten­sion .pdf, and when there is a Viewer Request.

Lambda@Edge Behavior

Once we pub­lish a ver­sion of our new func­tion, we will put the Function ARN string that we get from the func­tion dash­board, on the field Function ARN / Name on the Behav­ior of our Cloud­Front distribution.

Pub­lish­ing the Func­tion #

Once you are con­fi­dent with your code, have con­fig­ured the IAM Role and Per­mis­sion, have set up the behav­ior, and have done tests for dif­fer­ent pos­si­ble requests, it’s time to pub­lish your function. 

Option 1: #

Under Lamb­da -> Func­tions -> Your­Func­tion­Name, click the Action but­ton on the right top cor­ner of the screen. Then, there is two ways to do it:

  1. Select Publish new version, write some details descrip­tion for this version and click Publish
  2. Copy the val­ue under Function ARN, that’s the val­ue you will have to paste back in the Cloud­Front behav­ior you cre­at­ed, on the field Function ARN / Name and save.

Lambda@Edge Create Version

Now your func­tion is live. 

Option 2: #

Under Lamb­da -> Func­tions -> Your­Func­tion­Name, click the Action but­ton on the right top cor­ner of the screen. Then, there is two ways to do it:

  1. Select Deploy to Lambda@Edge
  2. Select your dis­tri­b­u­tion ID (you might need to open a new tab with your Cloud­Front dis­tri­b­u­tions to copy the dis­tri­b­u­tion ID) needed
  3. Write down your behav­iour, /*.pdf for this case
  4. For CloudFront event select Viewer Request
  5. Check Confirm deploy to Lambda@Edge
  6. Click Deploy

Lambda@Edge deploy to CloudFront

This auto­mat­i­cal­ly will update the Viewer Request under your behav­ior dash­boar to use the new version.

Now your func­tion is live. 

Edit your func­tion #

Notice how under your func­tion dash­board, there is a new ban­ner that says:

You can only edit your func­tion code or upload a new .zip or .jar file from the unpub­lished func­tion page.

Lambda@Edge Publish Function

To be able to edit this func­tion, you need to: 

  1. Click the but­ton Update code .
  2. Update your code
  3. Deploy
  4. Test
  5. Pub­lish Version
  6. Update Function ARN / Name on the Cloud­Front behav­ior you cre­at­ed to have the new version.
  7. Repeat as much as you need.

Why did we end up using Lambda@Edge? #

This client had about 2000 unique redi­rects to be val­i­dat­ed. These URLs would have to be defined on the vari­able brokenUrls of our code, due to Cloud­Front Func­tions size lim­i­ta­tions, this vari­able made the file too big for the avail­able scope.

If you have any ques­tions, feel free to con­tact me.


Claudia Aguilar

Partner, Software Engineer