Understanding the AWS Lambda Runtime API

Wojciech Matuszewski - Jun 19 '21 - - Dev Community

Many parts make the AWS Lambda running. Behind an arguably straightforward interface of a AWS Lambda handler stands a complex machinery that handles scaling, networking, isolation, and security.

The release of AWS Lambda extensions revealed some parts of that system to us, mere mortals using the platform. I'm talking about the Lambda Runtime API, the Lambda Logs API and the Lambda Extensions API.

This blog will focus on the Lambda Runtime API exploring what the Lambda Runtime API is and how it's used by the Lambda runtime your functions are operating on.

What the Runtime API is

In the context of AWS Lambda, the Runtime API is a bridge between the Lambda Service and the Lambda runtime your function is deployed with.

The Lambda Runtime API communicates with Lambda runtime through an HTTP layer.
This way of communication contrasts with how "older" runtimes operate. For example, the go1.x runtime uses a UDP transport layer with a proprietary data encoding scheme specific to the Go programming language (I suspect this is why the go1.x runtime does not support Lambda extensions).

The Lambda runtime pools the Runtime API for events then invokes your code with that event. Since the communication is bi-directional, the Lambda runtime communicates the invocation results and errors back to the Runtime API. Here is an illustration of how the flow looks like from my point of view.

Please note that this illustration is most likely a simplification. I'm in no way associated with the AWS Lambda team, and I do not know the service's technical inner workings. This is my mental model of how the AWS Lambda Runtime API communicates with Lambda runtime.

Communication

Custom runtimes and the Runtime API

The described communication mechanism between the Lambda runtime and the Runtime API enables us to create custom Lambda runtimes with relative ease. The first time I saw that it is possible to use bash as a Lambda runtime my mind was blown.

AWS has open-sourced some of the Lambda runtimes you might be using on a day-to-day basis. You can find the Go, Python and Node Lambda runtimes on their GitHub. I encourage you to go out and explore those repositories. There is much to learn about how the code you are writing and deploying is run.

Local development of custom runtimes

When developing in the cloud, there is no substitute for running your code in a real environment and communicating with actual services.
While this also stands true for developing custom Lambda runtime, there are two ways of emulating the Lambda Runtime API locally to shorten the feedback loop in-between deployments.

Using AWS SAM CLI

The AWS SAM CLI allows us to invoke given handler locally utilizing a Docker container. If we decide on developing a custom AWS Lambda runtime locally using AWS SAM, all we have to do is adhere to the AWS Lambda Runtime API specification and write a couple of lines of YAML.

Let us start with a AWS SAM template.



AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31

Description: An example serverless API and worker written in Golang.

Resources:
  Hello:
    Type: AWS::Serverless::Function
    Properties:
      # The custom runtime will be written in Go, thus the `provided.al2` runtime.
      Runtime: provided.al2
      Handler: bootstrap
      CodeUri: ./


Enter fullscreen mode Exit fullscreen mode

Next the runtime itself. For the sake of the example, the runtime loop was not implemented.



package main

import (
    "bytes"
    "context"
    "fmt"
    "net/http"
    "os"
)

// Huge simplification. Should not be used in production setting in any shape or form.
func main() {
    // As per documentation: https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html
    baseUrl := fmt.Sprintf("http://%v/2018-06-01/runtime/invocation", os.Getenv("AWS_LAMBDA_RUNTIME_API"))
    res, err := http.Get(baseUrl + "/next")
    if err != nil {
        panic(err)
    }
    defer res.Body.Close()

    reqId := res.Header.Get("Lambda-Runtime-Aws-Request-Id")

    out := handler(context.TODO())
    _, err = http.Post(fmt.Sprintf("%v/%v/response", baseUrl, reqId), "text/plain", bytes.NewReader([]byte(out)))
    if err != nil {
        panic(err)
    }
}

// Your deployed code.
func handler(ctx context.Context) string {
    return "Hello"
}


Enter fullscreen mode Exit fullscreen mode

The code follows the AWS Lambda runtime API documentation.

Before we invoke our custom runtime, we have to compile it.



GOARCH=amd64 GOOS=linux go build -o ./bootstrap bootstrap.go


Enter fullscreen mode Exit fullscreen mode

Please note that in the context of an actual deployment, the name of the binary does matter. I did not pick boostrap by random.
For additional resources regarding the naming, consult the Using custom runtime section of the revenant documentation.

All that is left for us to do with the binary created is to test our code. To do so, let us use the sam local invoke command.



sam local invoke --no-event Hello


Enter fullscreen mode Exit fullscreen mode

If you followed the steps carefully, the result of the invocation should be a plain string - Hello.

Using an emulator

Enter the aws-lambda-runtime-interface-emulator. This tool will allow us to emulate the AWS Lambda Runtime API locally (I suspect that the AWS SAM uses this tool under the hood as well). The aws-lambda-runtime-interface-emulator is designed to be used with Docker. Still, nothing stops us from containerizing our code for the sake of development and then, whenever we are ready, proceeding with deployment how we wish to. This local workflow is a bit more involved than the previous one, but it might be a valid alternative for those not using AWS SAM or are already using containers to deploy their Lambdas.

A great article already exists on setting up the aws-lambda-runtime-interface-emulator on the official AWS documentation site. The code that we have written before is fully compatible with the guide. If you want to give it a try, make sure not to skip the To add RIE to the image section.

Summary

I hope this journey into the AWS Lambda Runtime API was just as educating for you as it was for me. While I do not think that the knowledge presented here is necessary to develop AWS Lambda based applications effectively, I believe that there might be occasions when it's useful.
Weird edge cases happen, and knowing how the runtime of the language of your choice works might help you debug them more effectively.

Please do not hesitate to reach out. You can find me on twitter - @wm_matuszewski

Thank you for your time.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .