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!
NEW RESEARCH: LEARN HOW DECISION-MAKERS ARE PRIORITIZING DIGITAL INITIATIVES IN 2024.
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:
- is serverless, you don’t need to worry about managing a server for your code to run.
- can be triggered by an event such as a GET request, for example.
- supports a variety of languages.
- 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.
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.
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=https://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 https://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:
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:
Fill in the form with any URI parameters you need and hit the Test
button:
Bazinga! New stuff on the screen! Meemaw would be so proud.
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:
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!
Housni Yakoob
Related Posts
-
Promoting New Blog Entries with AWS Lambda and Slack
Here at Modus Create, we're avid users of Slack as a team collaboration tool and…
-
Build an Alexa Skill with Python and AWS Lambda
ul { list-style: circle outside; } Introduced in 2015, Amazon Echo is a wireless speaker…