Redirect Requests To A Domain With AWS Lambda

   DevOps
Redirects Requests to a Domain with AWS Lambda

The Problem

You’ve just purchased a snazzy new domain and you’ve decided to migrate your site to the new domain. But what happens when someone visits the old domain? No problem, you can just set up a web server running NGINX or Apache to redirect requests to the new domain. Easy, right?

Wrong!

Well, it’s not technically wrong but does it really make sense to use an entire web server to handle redirects? That’s like using an Echo Dot for a hockey puck. Total waste of resources…although that might be fun!

Think about it: a bunch of people visit your site every day. You don’t need a server, all you want is something to take a few milliseconds to quickly point them to another location and then go to sleep. Doesn’t that sound familiar?

If you’re thinking Amazon Web Services Lambda Functions, go get yourself a celebratory cookie! Get us one while you’re at it, too. Chocolate chip, please.

The Solution

Lambda is a serverless computing platform that can be triggered by events. You can code in C# (.NET Core), Go, Java, Node.Js, and Python. Plus, you only pay for the few milliseconds (in this case) it would be used.

Lambda:

  1. is serverless, you don’t need to worry about managing a server for your code to run.
  2. can be triggered by an event such as a GET request, for example.
  3. supports a variety of languages.
  4. has a tiered pricing structure where you only pay for what you use.

This is perfect for what we need. Especially the fact that it can be triggered by an event because we can use a request to AWS API Gateway to trigger the Lambda function.

API Gateway is exactly that: a gateway for your API. Based on the request path, you can configure it to accept specific HTTP methods, such as GET, POST, etc. or any HTTP methods.
For example, you can specify that GET requests to /pets are allowed but PUT requests to the same resource are rejected.

If you’ve developed RESTful web services, most of the frameworks out there let you define a route with which to access the backend (eg: the controller).

Think of API Gateway as the part that handles the routing and Lambda as the backend code. Now, your backend code doesn’t have to worry about routing, that is managed by API Gateway.

Managing the Solution with AWS Serverless Application Model (AWS SAM)

How can we manage these services? That’s where CloudFormation (CF) and AWS SAM (Serverless Application Model) come into play.

CF allows you to model and configure your AWS resources by declaratively describing your resources in a template. CF manages provisioning and configuring these resources. All the resources exist in a YAML or JSON template (IAC – Infrastructure as Code).

AWS SAM is CF on crack! SAM supports special resource types that makes defining resources a lot easier (less verbose).

If you’re familiar with CF templates, you’d know that it looks a bit like this:

AWSTemplateFormatVersion: '2010-09-09'
Resources:
  MyLambdaFunction:
    Type: AWS::Lambda::Function
    .
    .
    .

If you want to enable SAM in CF, all you have to do is insert Transform: AWS::Serverless-2016-10-31 into your CF template, like this:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
  MyLambdaFunction:
    Type: AWS::Lambda::Function
    .
    .
    .

That’s it! Now, your CF template is SAM enabled. That’s not all though. SAM makes deployments pretty simple. Using the cloudformation command via AWS CLI, you can package (package and upload local artifacts to S3) and deploy (deploys the CloudFormation template by creating and executing a changeset) with just two commands.

Code Time!

The stack should redirect the user to http://example.org for every incoming GET request. When redirecting, we should also append the path component and query string, if it exists.

In addition to the above, when the CF stack is deployed, a specific redirect domain (i.e. the new domain) and HTTP response code (301 or 302) should be specified.

Let’s get our hands dirty with some code. You can find all this code in the GitHub repository for this project,

From app_spec.yml:

---
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
  GetFunction:
    Type: AWS::Serverless::Function
    Properties:
      Description: "Handles GET requests."
      Handler: get.handler
      Runtime: nodejs6.10
      CodeUri: ./lambda
      Environment:
        Variables:
          NEW_DOMAIN: "http://example.org"
          HTTP_RESPONSE: 301

That’s a SAM template. It looks very similar to CF, doesn’t it? Two things that really stand out are the lines Transform: AWS::Serverless-2016-10-31 and AWS::Serverless::Function. The former allows you to use SAM features in this template and the latter is one of the 3 supported resource types for AWS SAM. As we’d do with CF, we define the Handler, Runtime, and other properties. However, CodeUri is specific to AWS::Serverless::Function. CodeUri specifies the location to the function code. This can either be the path on your hard disk or the full S3 URL to the Lambda deployment package.

Let’s have a look at our Lambda function:

From ./lambda/get.js:

'use strict';

var helper = require('./lib/helper');

/**
 * Handles GET requests.
 */
module.exports.handler = (event, context, callback) => {
    // Prevent crashing due to uncallable callbacks.
    // See: http://justbuildsomething.com/node-js-best-practices/#5
    callback = (typeof callback === 'function') ? callback : function() {};

    var query = helper.assembleQuery(event.queryStringParameters);
    var requestUri = helper.assembleRequest(event.path, query);
    var response = helper.assembleResponse(requestUri);

    callback(null, response);
};

From ./lambda/lib/helper.js:

/**
 * Assembles query parameters.
 * 
 * @param  {array}  queries The query string component from the request.
 * @return {string}         The query string joined using `&`.
 */
module.exports.assembleQuery = function (queries) {
    var query = [];
    for (var property in queries) {
        query.push(property + "=" + queries[property]);
    }
    return query.join('&');
};

/**
 * Assembles request path.
 * 
 * @param  {string} path  The path component of the request.
 * @param  {string} query The query string of the request.
 * @return {string}       The path and the query joined using `?`.
 */
module.exports.assembleRequest = function (path, query) {
    if (0 === Object.keys(query).length) {
        // There are no query vars so just use path.
        return path;
    }
    return [path, query].join("?");
}

/**
 * Assemble the response to send to API Gateway.
 * 
 * @param  {string} requestUri The path and the query joined using `?`.
 * @return {json}              The response sent to API Gateway via the Lambda proxy integration.
 *                             See: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format
 */
module.exports.assembleResponse = function (requestUri) {
    return {
        statusCode: process.env.HTTP_RESPONSE,
        headers: {
            "Location": process.env.NEW_DOMAIN + requestUri
        },
        body: null
    }
}

This assembles the path and query params and appends it to the new domain. It then combines that with the HTTP status code and sends that back to API Gateway.

We want to link the Lambda function to API Gateway. In the SAM template, we add the API Gateway as an event that will trigger the Lambda function. This is easily done by appending a few lines under Environment, like this:

From app_spec.yml:

---
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
  GetFunction:
    Type: AWS::Serverless::Function
    Properties:
      Description: "Handles GET requests."
      Handler: get.handler
      Runtime: nodejs6.10
      CodeUri: ./lambda
      Environment:
        Variables:
          NEW_DOMAIN: "http://example.org"
          HTTP_RESPONSE: 301
      Events:
        GetProxy:
          Type: Api
          Properties:
            Path: /{proxy+}
            Method: get
        GetCustom:
          Type: Api
          Properties:
            Path: /
            Method: get

We’ve told Lambda to accept two events that could trigger it. GetCustom will allow any GET requests to / to trigger Lambda. GetProxy will allow any GET requests to anything other than / to trigger Lambda. This covers all possible GET requests that can come to our site.
The beauty in this is that you don’t even have to explicitly define an API Gateway. The Lambda events we’ve declared causes CF to actually create the API for us.

During initial development, the Lambda function was defined without events and was defining the API in a swagger.yml file using the OpenAPI Specification and referencing that in our CF template. For our use case, that level of elaboration isn’t necessary so we stopped using swagger for this repo. If you do have specific requirements, you could weave a bit more magic into this with the swagger.yml file. If you want to look at how that was done, have a look at the ‘swagger’ tagged commit of the lambda-redirectory git repo. The deployment steps in that tag are different from what we’ll discuss later so make sure you have a look at the README for that tag, too.

The code is pretty small and our SAM template is only a few lines but you can deploy this and have our redirector work for all GET requests. Let’s deploy this!

First, we package and upload local artifacts to an S3 bucket named MyRedirectorBucket (make sure the bucket exists first!):

$ aws cloudformation package \
    --template-file app_spec.yml \
    --output-template-file packaged.yml \
    --s3-bucket MyRedirectorBucket

Successfully packaged artifacts and wrote output template to file packaged.yml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /home/housni/modus-create/lambda-redirector/packaged.yml --stack-name <YOUR STACK NAME>

$ aws cloudformation deploy \
    --template-file packaged.yml \
    --stack-name Redirector \
    --capabilities CAPABILITY_IAM

Read more about packaging and deployment.

The package command simply zips up the lambda dir and uploads it to an S3 bucket called MyRedirectorBucket. It also generates a file named packaged.yml which is a processed version of app_spec.yml.

Next, we deployed our stack using the processed template, packaged.yml.
The deploy command creates and executes a changeset based on your template, packaged.yml. Since we give it the CAPABILITY_IAM capability, it can also manage the permissions it needs to create this stack by creating IAM users, for example. If you look at your CF stack via the AWS console, you’ll see a tab named Resources. Always examine that tab. Make sure you understand what each resource is. Notice there are a bunch of AWS::Serverless::Permissions typed resources. Those were not specified in our SAM templates but because we gave it the CAPABILITY_IAM capability, it’s able to create the necessary permissions such as allowing API Gateway to invoke our Lambda function and also allowing Lambda to log activity to CloudWatch.

Redirects Requests to a Domain with AWS Lambda, CloudFormation Resources

Of course, if you want to, you can go ahead and modify the template to add permissions using the CloudFormation template syntax but for the purpose of this post, this is sufficient.

Once you’ve finished running the above CloudFormation commands, you’ll have a fully deployed stack!

Let’s test this! But we’ll need to get the URL of the API to test it.

Go over to your API Gateway console screen, slick on Stages and then Prod. One the right pane, you should see the Invoke URL at the top. Prod is the stage to which the SAM template deployed the API to.

Redirects Requests to a Domain with AWS Lambda, API Gateway Invoke URL

We can run a curl request against that URL to make sure everything’s working:

$ curl \
    -X GET \
    -s \
    -w "%{url_effective}\n%{http_code}\n%{redirect_url}" \
    "https://3obl3k5ugh.execute-api.us-east-1.amazonaws.com/Prod"
https://3obl3k5ugh.execute-api.us-east-1.amazonaws.com/Prod
301
http://example.org

The curl output shows us the URL we requested, https://3obl3k5ugh.execute-api.us-east-1.amazonaws.com/Prod, and the 301 response for http://example.org.

Looks like things are working fine!

NOTE: In addition to the Prod stage, you will also see a stage named Stage. That’s a bug in SAM that’s currently being worked on. The good news is, it doesn’t get in the way of anything.

Intrinsic Functions for User Input

Intrinsic Functions allows us to specify values that will be available to our deployment at runtime. We can use this to specify the values for the environment variables NEW_DOMAIN and HTTP_RESPONSE at runtime.

From app_spec.yml:

---
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Parameters:
  HTTPResponse:
    Type: Number
    Default: 302
    AllowedValues: [301, 302]
    Description: "The HTTP response status code."
  NewDomain:
    Type: String
    Description: "New domain to redirect to."
Resources:
  GetFunction:
    Type: AWS::Serverless::Function
    Properties:
      Description: "Handles GET requests."
      Handler: get.handler
      Runtime: nodejs6.10
      CodeUri: ./lambda
      Environment:
        Variables:
          NEW_DOMAIN: !Ref NewDomain
          HTTP_RESPONSE: !Ref HTTPResponse
      Events:
        GetProxy:
          Type: Api
          Properties:
            Path: /{proxy+}
            Method: get
        GetCustom:
          Type: Api
          Properties:
            Path: /
            Method: get

We’ve told our SAM template to expect two parameters HTTPResponse and NewDomain and we reference those parameters when defining our environment variables. !Ref then replaces the environment variables with the value of the parameters we entered.

Let’s deploy our now, altered stack, with some custom values and see if that works:

$ aws cloudformation package \
    --template-file app_spec.yml \
    --output-template-file packaged.yml \
    --s3-bucket MyRedirectorBucket

Successfully packaged artifacts and wrote output template to file packaged.yml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /home/housni/modus-create/lambda-redirector/packaged.yml --stack-name <YOUR STACK NAME>
$ aws cloudformation deploy \
    --template-file packaged.yml \
    --stack-name Redirector \
    --capabilities CAPABILITY_IAM \
    --parameter-overrides \
        NewDomain=http://moduscreate.com \
        HTTPResponse=302

Now let’s curl our API:

$ curl \
    -X GET \
    -s \
    -w %{url_effective}\n%{http_code}\n%{redirect_url}" \
    "https://3obl3k5ugh.execute-api.us-east-1.amazonaws.com/Prod"
https://3obl3k5ugh.execute-api.us-east-1.amazonaws.com/Prod
302

Now, we’ve got a 302 redirect to http://moduscreate.com.

That’s pretty much it! In the final lambda-redirector repo on Github.com, we’ve optimized our Lambda functions a bit and added more resources to our SAM stack to route only GET requests to one function and requests of all other HTTP methods to another. Apart from that, the code is pretty much the same thing!

Protect ya Neck!

If you ever alter the code/SAM template, you might want to test or debug things. There are a bunch of useful ways to do this.

CloudWatch for Lambda

If you need to debug your Lambda functions, a simple enough way to do this is to use console.log() to spit out some values. You can modify the lambda/get.js function to have console.log(event.queryStringParameters); on the first line of the handler ran curl with:

$ curl \
    -X GET \
    -s \
    -w "%{url_effective}\n%{http_code}\n%{redirect_url}" \

"https://3obl3k5ugh.execute-api.us-east-1.amazonaws.com/Prod/test/path?key1=val1&key2=val2&key3=val3"

And the CloudWatch log for the Lambda function shows:

Redirects Requests to a Domain with AWS Lambda, Debug with CloudWatch

 

API Gateway Console

For the API Gateway, you can debug right from within the API Gateway console, which provides all the output you’d need to test/debug the API.

Click on TEST for the resource you want to test:

Redirects Requests to a Domain with AWS Lambda, Test API Gateway Endpoint

Fill in the form with any URI parameters you need and hit the Test button:

Redirects Requests to a Domain with AWS Lambda, Run API Gateway Endpoint

Bazinga! New stuff on the screen! Meemaw would be so proud.

Redirects Requests to a Domain with AWS Lambda, API Gateway Endpoint Test Results

 

AWS CLI

You can also view the same output as the API Gateway Console if you used AWS CLI to invoke your endpoints:

Figure out your rest-api-id and resource-id by looking at the values in the breadcrumb at the top of the page as shown:

Redirects Requests to a Domain with AWS Lambda, API Gateway Test Invoke Method Parameters

Run the apigateway test-invoke-method with the appropriate parameters:

$ aws apigateway test-invoke-method \
    --rest-api-id 3obl3k5ugh \
    --resource-id 5z8bpy \
    --http-method GET \
    --path-with-query-string '/test/path?key1=val1&key2=val2&key3=val3'

The output will be the same thing you’d see via the console in JSON format.

SAM Local

SAM Local is a CLI tool which uses Docker to run code in local containers that allows you to quickly test and develop your serverless applications. You can read more about how to test your SAM applications locally and check out the repo with the Go code.

Conclusion

Instead of dedicating an entire web server to redirect our requests, we were able to do this with API Gateway and Lambda functions. Not only that, but we were able to create it using CloudFormation templates which means we can version control our infrastructure. Using CloudFormation parameters, we were also able to specify the new domain to redirect to and the HTTP status code while deploying which makes setting up custom redirectors a lot easier to repeat. There‚Äôs always space for improvement, so if you have alternative ways of doing this, we’d love to hear about it in the comments!


This website uses cookies

These cookies are used to collect information about how you interact with our website and allow us to remember you. We use this information in order to improve and customize your browsing experience, and for analytics and metrics about our visitors both on this website and other media. To find out more about the cookies we use, see our Privacy Policy.

Please consent to the use of cookies before continuing to browse our site.

Like What You See?

Got any questions?


>