Using Lambda@Edge to handle complex redirect rules with CloudFront

12.06.2019

Chris Mckinnel

 

Problem

Most mature CDNs on the market today offer the capability to define URL forwarding / redirects using path based rules that get executed on the edge, minimising the wait time for users to be sent to their final destination.

CloudFront, Amazon Web Services’ CDN offering, provides out-of-the box support for redirection from HTTP to HTTPS and will cache 3xx responses from its origins, but it doesn’t allow you to configure path based redirects. In the past, this meant we had to configure our redirects close to our origins in specific AWS regions which had an impact on how fast we could serve our users content.

 

Solution

Luckily, AWS has anticipated this as a requirement for its users and provides other services in edge locations that can compliment CloudFront to enable this functionality.

AWS Lambda@Edge is exactly that, a lambda function that runs on the edge instead of in a particular region. AWS has the most comprehensive Global Edge Network with, at the time of writing, 169 edge locations around the world. With Lambda@Edge, your lambda function runs in a location that is geographically closest to the user making the request.

You define, write and deploy them exactly the same way as normal lambdas, with an extra step to associate them with a CloudFront distribution which then copies them to the edge locations where they’ll be executed.

Lambda@Edge can intercept requests at different stages of the request life-cycle:

For our use case, we want to intercept the viewer request and redirect the user based on a set of path based rules.

In the following section there is instructions on how to deploy implement redirects at the edge using the Serverless Application Model, CloudFront and Lambda@Edge.

 

Assumptions

This guide is written with the assumption that you have the following things set up:

Since we’ll be using the Serverless Application Model to define and deploy our lambda, we’ll need to set up an S3 bucket for sam package, so we have a prerequisites CloudFormation template.

Note: everything in this guide is deployed into us-east-1. I have included the region explicitly in the CLI commands, but you can use your AWS CLI config if you want (or any of the other valid ways to define region).

 

1) Create the following file:

lambda-edge.prerequisites.yaml

AWSTemplateFormatVersion: '2010-09-09'

Resources:

  RedirectLambdaBucket:

    Type: AWS::S3::Bucket

Outputs:

  RedirectLambdaBucketName:

    Description: Redirect lambda package S3 bucket name

    Value: !Ref RedirectLambdaBucket

We define the bucket name as an output so we can refer to it later.

 

2) Deploy the prerequisites CloudFormation stack with:

$ aws --region us-east-1 cloudformation create-stack --stack-name redirect-lambda-prerequisites --template-body file://`pwd`/lambda-edge-prerequisites.yaml

This should give you an S3 bucket we can point sam deploy to, let’s save it into an environment variable so it’s easy to use in future commands (you can also just get this from the AWS Console):

 

3) Run the following command:

$ export BUCKET_NAME=$(aws --region us-east-1 cloudformation describe-stacks --stack-name redirect-lambda-prerequisites --query "Stacks[0].Outputs[?OutputKey=='RedirectLambdaBucketName'].OutputValue" --output text)

Now we’ve got our bucket name ready to use with $BUCKET_NAME, we’re ready to start defining our lambda using the Serverless Application Model.

The first thing we need to define is a lambda execution role. This is the role that our edge lambda will assume when it gets executed.

 

4) Create the following file:

lambda-edge.yaml

AWSTemplateFormatVersion: '2010-09-09'

Transform: AWS::Serverless-2016-10-31

Description: Full stack to demo Lambda@Edge for CloudFront redirects

 

Parameters:

  RedirectLambdaName:

    Type: String

    Default: redirect-lambda

 

Resources:

  RedirectLambdaFunctionRole:

    Type: AWS::IAM::Role

    Properties:

      AssumeRolePolicyDocument:

        Version: '2012-10-17'

        Statement:

          - Effect: Allow

            Principal:

              Service:

                - 'lambda.amazonaws.com'

                - 'edgelambda.amazonaws.com'

            Action:

              - 'sts:AssumeRole'

      ManagedPolicyArns:

        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'

Notice that we allow both lambda.amazonaws.com and edgelambda.amazonaws.com to assume this role, and we grant the role the AWSLambdaBasicExecutionRole managed policy, which grants it privileges to publish its logs to CloudWatch.

Next, we need to define our actual lambda function using the Serverless Application Model.

 

5) Add the following in the Resources: section of lambda-edge.yaml:

lambda-edge.yaml

  RedirectLambdaFunction:

    Type: AWS::Serverless::Function

    Properties:

      CodeUri: lambdas/

      FunctionName: !Ref RedirectLambdaName

      Handler: RedirectLambda.handler

      Role: !GetAtt RedirectLambdaFunctionRole.Arn

      Runtime: nodejs10.x

      AutoPublishAlias: live

Note: we define AutoPublishAlias: live here which tells SAM to publish both an alias and a version of the lambda and link the two. CloudFront requires a specific version of the lambda and doesn’t allow us to use $LATEST.

We also define CodeUri: lambdas/ which tells SAM where it should look for the Node.js that will be the brains of the lambda itself. This doesn’t exist yet, so we’d better create it:

 

6) Make a new directory called lambdas:

$ mkdir lambdas

 

7) Inside that directory, create the following file:

lambdas/RedirectLambda.js

'use strict';

exports.handler = async (event) => {

    console.log('Event: ', JSON.stringify(event, null, 2));

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

    const redirects = {

        '/path-1':    'https://consegna.cloud/',

        '/path-2':    'https://www.amazon.com/',

    };

    if (redirects[request.uri]) {

        return {

            status: '302',

            statusDescription: 'Found',

            headers: {

                'location': [{ value: redirects[request.uri] }]

            }

        };

    }

    return request;

};

The key parts of this lambda are:

We can inspect the viewer request as it gets passed in via the event context,

We can return a 302 redirect if the request path meets some criteria we set, and

We can return the request as-is if it doesn’t meet our redirect criteria.

You can make the redirect rules as simple or as complex as you like.

 

You may have noticed we hard-code our redirect rules in our lambda, we do this for a couple of reasons but you may decide you’d rather keep your rules somewhere else like DynamoDB or S3. The three main reasons we have our redirect rules directly in the lambda are:

The quicker we can inspect the request and return to the user the better, having to hit DynamoDB or S3 will slow us down

Because this lambda is executed on every request, there will be cost implications to hit DynamoDB or S3 every time

Defining our redirects via code means we can have robust peer reviews using things like GitHub’s pull requests

Because this is a Node.js lambda, SAM requires us to define a package.json file, so we can just define a vanilla one:

 

8) Create the file package.json:

lambdas/package.json

{

  "name": "lambda-redirect",

  "version": "1.0.1",

  "description": "Redirect lambda using Lambda@Edge and CloudFront",

  "author": "Chris McKinnel",

  "license": "MIT"

}

The last piece of the puzzle is to define our CloudFront distribution and hook up the lambda to it.

 

9) Add the following to your lambda-edge.yaml:

 

lambda-edge.yaml

  CloudFront:

    Type: AWS::CloudFront::Distribution

    Properties:

      DistributionConfig:

        DefaultCacheBehavior:

          Compress: true

          ForwardedValues:

            QueryString: true

          TargetOriginId: google-origin

          ViewerProtocolPolicy: redirect-to-https

          DefaultTTL: 0

          MaxTTL: 0

          MinTTL: 0

          LambdaFunctionAssociations:

            - EventType: viewer-request

              LambdaFunctionARN: !Ref RedirectLambdaFunction.Version

        Enabled: true

        HttpVersion: http2

        PriceClass: PriceClass_All

        Origins:

          - DomainName: www.google.com

            Id: google-origin

            CustomOriginConfig:

              OriginProtocolPolicy: https-only

In this CloudFront definition, we define Google as an origin so we can define a default cache behaviour that attaches our lambda to the viewer-request. Notice that when we associate the lambda function to our CloudFront behaviour we refer to a specific lambda version.

 

SAM / CloudFormation template

 

Your SAM template should look like the following:

 

lambda-edge.yaml

AWSTemplateFormatVersion: '2010-09-09'

Transform: AWS::Serverless-2016-10-31

Description: Full stack to demo Lambda@Edge for CloudFront redirects

Parameters:

  RedirectLambdaName:

    Type: String

    Default: redirect-lambda

Resources:

  RedirectLambdaFunctionRole:

    Type: AWS::IAM::Role

    Properties:

      AssumeRolePolicyDocument:

        Version: '2012-10-17'

        Statement:

          - Effect: Allow

            Principal:

              Service:

                - 'lambda.amazonaws.com'

                - 'edgelambda.amazonaws.com'

            Action:

              - 'sts:AssumeRole'

      ManagedPolicyArns:

        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'

 

  RedirectLambdaFunction:

    Type: AWS::Serverless::Function

    Properties:

      CodeUri: lambdas/

      FunctionName: !Ref RedirectLambdaName

      Handler: RedirectLambda.handler

      Role: !GetAtt RedirectLambdaFunctionRole.Arn

      Runtime: nodejs10.x

      AutoPublishAlias: live

 

  CloudFront:

    Type: AWS::CloudFront::Distribution

    Properties:

      DistributionConfig:

        DefaultCacheBehavior:

          Compress: true

          ForwardedValues:

            QueryString: true

          TargetOriginId: google-origin

          ViewerProtocolPolicy: redirect-to-https

          DefaultTTL: 0

          MaxTTL: 0

          MinTTL: 0

          LambdaFunctionAssociations:

            - EventType: viewer-request

              LambdaFunctionARN: !Ref RedirectLambdaFunction.Version

        Enabled: true

        HttpVersion: http2

        PriceClass: PriceClass_All

        Origins:

          - DomainName: www.google.com

            Id: google-origin

            CustomOriginConfig:

              OriginProtocolPolicy: https-only

 

And your directory structure should look like:

├── lambda-edge-prerequisites.yaml

├── lambda-edge.yaml

├── lambdas

│   ├── RedirectLambda.js

│   └── package.json

└── packaged

    └── lambda-edge.yaml

 

Now we’ve got everything defined, we need to package it and deploy it. AWS SAM makes this easy.

 

10) First, create a new directory called package:

$ mkdir package

 

11) Using our $BUCKET_NAME variable from earlier, we can now run:

$ sam package --template-file lambda-edge.yaml --s3-bucket $BUCKET_NAME > packaged/lambda-edge.yaml

 

The AWS SAM CLI takes the local SAM template and parses it into a format that CloudFormation understands. After running this command, you should have a directory structure like this:

├── .aws-sam

│   └── build

│       ├── RedirectLambda

│       │ ├── RedirectLambda.js

│       │ └── package.json

│       └── template.yaml

├── lambda-edge-prerequisites.yaml

├── lambda-edge.yaml

├── lambdas

│   ├── RedirectLambda.js

│   └── package.json

└── packaged

    └── lambda-edge.yaml

Notice the new .aws-sam directory – this contains your lambda code and a copy of your SAM template. You can use AWS SAM CLI to run your lambda locally, however this is out of the scope of this guide. Also notice the new file under the packaged directory – this contains direct references to your S3 bucket, and it’s what we’ll use to deploy the template to AWS.

You can find the full demo, downloadable in zip format, here: lambda-edge.zip

Finally we’re ready to deploy our template.

 

12) Deploy your template by running:

$ sam deploy --region us-east-1 --template-file packaged/lambda-edge.yaml --stack-name lambda-redirect --capabilities CAPABILITY_IAM

 

Note the –capabilities CAPABILITY_IAM, this tells CloudFormation that we acknowledge that this stack may create IAM resources that may grant privileges in the AWS account. We need this because we’re creating an IAM execution role for the lambda.

 

This should give you a CloudFormation stack with a lambda deployed on the edge that is configured with a couple of redirects.

When you hit your distribution domain name and append a redirect path (/path-2/ – look for this in the lambda code), you should get redirected:

Summary

AWS gives you building blocks that you can use together to build complete solutions, often these solutions are much more powerful than what’s available out-of-the-box in the market. Consegna has a wealth of experience designing and building solutions for their clients, helping them accelerate their adoption of the cloud.