Lambdaless

Lets assume you need to expose a JSON file behind an API. Using a serverless approach with AWS, you might first reach for an architecture like the following:

API Gateway to Lambda to S3

… i.e. an API Gateway in front of a Lambda, which calls S3. Alternatively, did you know you could remove the Lambda and have API Gateway call S3 directly?

API Gateway to Lambda

This is what I call “Lambdaless”. It leverages API Gateway’s AWS integration type, which allows you to expose any AWS service without any intermediate application logic. Mapping templates provide the glue to transform request/responses, using the Velocity templating language (VTL) and JSONPath expressions.

Walkthrough

Continuing with the S3 example above, create an API Gateway with a GET method and set up the integration request per the following:

API Gateway S3 Integration Request

Create an IAM role that has a policy that has s3:GetObject permission on your <bucket>/<prefix> and a Trust Relationship that allows the API Gateway to assume it to be so. Now all you need to do is switch to the test view, click “test” and you should see the contents of your JSON object in the response body:

API Gateway S3 Request

Examples

Mock integration

Taking the JSON example to its logical conclusion, we can go a step further and remove S3 from the equation altogether. Choose the MOCK integration type, add the required {"statusCode": 200} request mapping template and move the contents of your JSON object to the integration response mapping template.

This approach typically yields ~3ms response times (compared to ~65ms with the additional hop to S3) and is a good solution for static data.

DynamoDB

Simple CRUD APIs with DynamoDB are a great fit for Lambdaless. API Gateway’s $context variables includes $context.requestId, which can be used as a entity’s UUID, along with $context.requestTimeEpoch for created/updated at timestamps.

Request/response templates can be used to convert to/from DynamoDB’s data type descriptors, for example:

#set($inputRoot = $input.path('$'))
{
  "TableName": "my-table",
  "Key": {
    "uuid": {
      "S": "$context.requestId"
    }
  },
  "Item": {
    "uuid": {
      "S": "$context.requestId"
    },
    "name": {
      "S": "$inputRoot.name"
    },
    "items": {
      "L": [
        #foreach($item in $inputRoot.items)
        {
          "S": "$item"
        }#if($foreach.hasNext),#end
        #end
      ]
    },
    "createdAt": {
      "N": "$context.requestTimeEpoch"
    }
  }
}

Other ideas

Advantages

A simple Lambda may seem innocuous at first, but each function comes with their own maintenance cost including:

Removing a Lambda means fewer resources to maintain, test and pay for. Latency is also reduced. There are less hops in the chain and the issue of cold starts disappears.

Disadvantages

There are however a number drawbacks to consider with this Lambdaless method. Probably most apparent is the fact that you can only integrate with a single service at a time. This limits the approach to simple integrations and rules out complex logic e.g. joins.

Velocity, whilst offering some level of control flow such as if/else and loops, as well as AWS’s own extensions such as util functions, is somewhat of a niche language and introduces its own complexity over using your Lambda runtime language of choice (e.g. JavaScript, Python).

This approach is also tightly coupled with API Gateway. The AWS integration type and request/response mapping template approach is unique to API Gateway and therefore is less portable than Lambda application logic (which is easier to abstract from the Lambda environment itself).

It also relies on “low-level” AWS APIs, which are less accessible and often sparsely documented compared to their corresponding SDK wrappers.

Further reading

Thanks to Callum Vincent for reading drafts of this.