AWS Yarns

A blog about AWS things.

Use Lambda@Edge to handle complex redirect rules with CloudFront

Posted by Chris McKinnel - 12 June 2019
8 minute read

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:

Lambda@Edge request types. CloudFront serving our website from S3 will be fully serverless.

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.

How to

Assumptions

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

  • An AWS account
  • AWS CLI
  • AWS SAM CLI

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:

  1. We can inspect the viewer request as it gets passed in via the event context,
  2. We can return a 302 redirect if the request path meets some criteria we set, and
  3. 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:

  1. The quicker we can inspect the request and return to the user the better, having to hit DynamoDB or S3 will slow us down
  2. Because this lambda is executed on every request, there will be cost implications to hit DynamoDB or S3 every time
  3. 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.yaml

├── 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:

lambda-edge.yaml

├── .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.

Lambda@Edge redirect CloudFormation stack.

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:

Lambda@Edge redirect request headers.

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.

Note: This blog post was originally published on the Consegna blog, authored by Chris McKinnel. It has been re-posted here with permission.

https://blog.consegna.cloud/2019/06/12/use-lambdaedge-to-handle-complex-redirect-rules-with-cloudfront/