ALB Triggers for a Serverless Web App

What we learned building a web app with AWS Lambda, API Gateway, and Application Load Balancers

Andreea Paduraru
Expedia Group Technology
6 min readJul 2, 2019

--

The Big Data Platform team at Hotels.com had the chance of trying out AWS infrastructure and seeing what the best fit for deploying a simple, stateless, serverless web application could be. Our initial plan was to use AWS API Gateway to set up a REST endpoint for the API and then trigger the business logic coded up as an AWS Lambda. After some exploration, testing and further gathering of requirements, we realized that using an Application Load Balancer instead of API Gateway was a much better fit for our use-case.

This post will take you through our experiences with both, and it will highlight the steps that took us from one choice to the other.

AWS API Gateway

API Gateway is an Amazon service which creates, manages and secures APIs. An API is made up of resources organized in a tree structure which have methods that are integrated with the back-end. There are multiple types of integration, two of which are dedicated for integrating with a Lambda function:

  1. Lambda custom integration - this allows you to modify the request before sending it to the Lambda, and also the response after being returned from the Lambda.
  2. Lambda proxy integration - here the request is sent straight to the Lambda and the response is sent straight back, with no transformations happening in API Gateway.

There is also a way of defining a proxy resource, corresponding to a greedy path variable, which routes multiple paths to a single integration. If you want API Gateway to simply send any request to a Lambda, you can use this in conjunction with the proxy integration.

Our use-case

We started by only having one type of request - one method integrated with just one Lambda, so whether we chose the custom or the proxy integration didn’t make much difference since we also didn’t want to perform any transformation on the request or response. This was working fine at the beginning, but then we realized we actually have three different types of requests we want to support, which would mean three different integrations with three different Lambdas.

The problem was that all three calls we wanted to support could be handled using the same Lambda code, so we didn’t want to deploy three different instances of it. Also, if we only had one Lambda, we would be three times less likely to encounter the unwanted cold start behaviour (the first request takes a long time to complete because of the Lambda initialization overhead).

So we decided to use the proxy resource + proxy integration approach mentioned above, so that we would proxy all requests to one Lambda. This meant that the parsing of the request path and parameters had to be done in the Lambda code in order to route the requests accordingly. We used Spring MVC and the aws-serverless-java-container library so that the Spring application can be run inside the Lambda and is compatible with API Gateway.

Example Java Lambda handler code using the library above:

public class StreamLambdaHandler implements RequestStreamHandler {
private static SpringLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
static {
try {
handler = SpringLambdaContainerHandler.getAwsProxyHandler(ExampleSpringAppConfig.class);
} catch (ContainerInitializationException e) {
throw new RuntimeException("Could not initialize Spring framework", e);
}
}

@Override
public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException {
handler.proxyStream(inputStream, outputStream, context);
}
}

At this point we coded up our business logic and had a way of accessing it via an API, so it was time to deploy it in our environments. We wanted it to have a custom domain name which was user-friendly and didn’t change whenever a new deployment was made. But one requirement was that the API had to be private, and we later found out that custom domain names were not supported for private APIs.

This is the format of an invoke URL for an API deployed with API Gateway:

https://{restapi-id}.execute-api.{region}.amazonaws.com/{stage} 

Which in practice could turn to:

https://0qzs2sy7bh.execute-api.us-west-2.amazonaws.com/test

This meant that we had to either settle for using the invoke URL containing a randomly generated ID at deployment, or look for an alternative to API Gateway. The users of our application couldn’t reasonably be expected to keep changing the URLs for the application endpoint, so the search for a different approach began.

Using an Application Load Balancer (ALB) as a Lambda trigger

We found out that quite recently (end of November 2018), AWS started supporting Lambda functions as targets for Application Load Balancers. An ALB is a single entry point which routes the traffic to targets such as EC2 instances, and now also Lambda functions.

It turned out that this was exactly what we needed — a way of invoking the Lambda in a simple way and to use Amazon Route 53 to have a stable domain name which routes traffic to the ALB. So the problem mentioned above would be solved by replacing API Gateway with an ALB, and for that to happen, we needed to upgrade the version of the aws-serverless-java-container library to the latest one, since the older version we were using only supported API Gateway, but not an ALB:

<dependency>
<groupId>com.amazonaws.serverless</groupId>
<artifactId>aws-serverless-java-container-spring</artifactId>
<version>1.3.1</version>
</dependency>

There was also a “multi-value-headers” setting in the ALB’s Lambda target group which we had to enable in order to have the query string parameters passed to the Lambda, even though the name of the setting didn’t suggest that.

There are a few differences in how API Gateway and an ALB interact with the Lambda, but in our case, we ended up disconnecting the Lambda from a normal API Gateway use-case almost entirely, so it wasn’t hard to make the transition to the ALB.

Another advantage that the ALB provided in our case was that it supported health checks on the targets, which would periodically trigger the Lambda, and that meant it would always be warm. Since we start a Spring application context in our Lambda code, we knew we had a potential problem with cold starts as this is typically an expensive and relatively slow operation. Switching to the ALB had a nice side effect that we no longer had this issue.

Conclusions

We eventually found a solution which satisfied our requirements, but along the way we discovered quite a lot of interesting aspects about deploying a Lambda using both API Gateway and an ALB. Deciding to go with the ALB doesn’t mean that API Gateway doesn’t have its useful features, but that our use-case didn’t allow us to take advantage of them. API Gateway has support for Authentication and Authorization, as well as request and response mapping which might prove useful in other situations, but for the most part, it is interchangeable with an ALB, and the Lambda code shouldn’t be affected by this choice.

What we found is that keeping up with the latest changes and updates is quite important, as seen in the case of not using the latest version of the library mentioned, as well as not taking into consideration the ALB option earlier.

Looking back, introducing Spring in the Lambda code in order to not have three individual Lambdas deployed and three different cold starts might not have been the best decision, as this actually caused the start-up time to increase further. It might also be worth considering that if we had three Lambdas, we would also have 3 million free requests per month, as AWS provides 1 million free requests per month for every Lambda, but this depends on how the application is intended to be used, so it could be worth experimenting and seeing which option works best for your use-case.

--

--