Over the weekend, I spent some time experimenting with the new Runtime API and Lambda Layer that AWS announced last week. The goal was to create a custom runtime that I could use to start writing Lambda functions in PHP. Following is a brief summary of what I learned in the process and steps to create your own custom runtime.
Source for the code samples used in this post: https://github.com/pagnihotry/PHP-Lambda-Runtime
Getting started
There are a couple of offerings from AWS that you would need to be familiar with for creating a custom runtime and to add support for any language on Lambda:
- Lambda Layer - AWS allows users to bundle some files (code or libraries) separate from the main function. This is called Lambda Layer. The advantage of using Layer is that you can create it and reuse in as many functions as you like, keeping the function itself small and enabling easy reuse of the common code or libraries.
- Runtime API - This is the API that allows a Layer (or Function) to interact with AWS Lambda like send response or error to it or check if a new event is available to process. More on Runtime API: AWS Lambda Runtimes.
Combining these 2, you can add re-usable support for pretty much any language or possibly create custom runtimes with baked in libraries, etc. that you can use in multiple functions or distribute to your users.
Note: You can add multiple layers in a function and specify the order in which you would like to process them.
Adding Custom Runtime
To add a custom runtime that can be shared across functions, you will first need to create a Layer and then implement the logic to process the incoming invocations.
While creating a custom runtime, Lambda expects certain files and logic to be present in the uploaded zip file. It expects a file called bootstrap
with permissions 755
. This file should either implement or start an endless loop fetching incoming invocations of Lambda from the Runtime API and report the response or errors to it.
Once the instace serving Lambda starts, bootstrap
is moved to /opt/bootstrap
. I also noticed that it moved all the files I had included in my zip file to the /opt
directory. The bootstrap
file is executed exactly once - at the startup time. Hence, the endless loop is needed to keep processing the incoming requests. If the runtime stops, you would see your Lambda functions returning an error.
Available environment variables
There are certain environment variables which are available to the Layer that I have used in my implementation:
LAMBDA_TASK_ROOT
- The local directory containing the function codeAWS_LAMBDA_RUNTIME_API
- HOST:PORT for the runtime API_HANDLER
- The function handler defined in the Lambda config
A full list of all the environment variables that are available can be found here: Lambda Execution Environment and Available Libraries
bootstrap
The first step for making your own Layer will be to put together the required files. As mentioned earlier in this post, you will need a bootstrap
file. In my setup, the bootstrap file executes a bundled PHP script using a static PHP executable that I compiled from source on alinux 2017.03.
bootstrap
(https://github.com/pagnihotry/PHP-Lambda-Runtime/blob/master/runtime/bootstrap)
#!/bin/sh
#go into the source directory
cd $LAMBDA_TASK_ROOT
#execute the runtime
/opt/php /opt/runtime.php
Note: More time you spend in initialization step of your bootstrap, higher will be the cold start time of your Lambda function.
runtime.php
The runtime.php
file contains the logic to curl the Runtime API, checking for available Lambda invocations and then calling the handler with the payload data followed by reporting the results to the Runtime API.
When a Lambda function is invoked, the payload becomes available on the /runtime/invocation/next
endpoint of the API. From here on, the runtime code fetches the event, runs the handler set in the Lambda configuration and reports the response to /runtime/invocation/<AwsRequestId>/response
or the error to /runtime/invocation/<AwsRequestId>/error
. Once the response or error endpoints are hit, Lambda will return/display success or error to the invoking service.
If for some reason, error or response endpoints do not get a hit, I noticed that this loop keeps running invoking the handler multiple times till the maximum execution time of the Lambda is reached and then returns with an error. You can try it out by commenting flushResponse
call in the provided example.
runtime.php
(https://github.com/pagnihotry/PHP-Lambda-Runtime/blob/master/runtime/runtime.php)
$lambdaRuntime = new LambdaRuntime();
$handler = $lambdaRuntime->getHandler();
//Extract file name and function
list($handlerFile , $handlerFunction) = explode(".", $handler);
//Include the handler file
require_once($handlerFile.".php");
//Poll for the next event to be processed
while (true) {
//Get next event
$data = $lambdaRuntime->getNextEventData();
//Check if there was an error that runtime detected with the next event data
if(isset($data["error"]) && $data["error"]) {
continue;
}
//Process the events
$eventPayload = $lambdaRuntime->getEventPayload();
//Handler is of format Filename.function
//Capture stdout
ob_start();
//Execute handler
$functionReturn = $handlerFunction($eventPayload);
$out = ob_get_clean();
$lambdaRuntime->addToResponse($out.$functionReturn);
//Report result
$lambdaRuntime->flushResponse();
}
The layout of the zip file uploaded to the Layer:
.
├── bootstrap
├── php
└── runtime.php
Note all the files are at the root. Not all files are required to be there except for
bootstrap
. Lambda expects thebootstrap
file to be in the root of the uploaded zip.
Creating the Layer in AWS
Once the code and file are in place, creating a new Layer can be done using the CLI or the UI.
From the UI
- Navigate to AWS Lambda and Create a new Layer
- Upload the zip file
- Leave the
Compatible runtimes
empty - Press
Create
AWS CLI
aws lambda publish-layer-version --layer-name <name of the layer> --zip-file fileb://<path to the zip file>
Adding a function that uses the custom Layer
Following are the steps to create the function that uses a custom runtime from the Layer created in the section above.
From the UI
- Select
Author from scratch
- Select
Use custom runtime in function code or layer
- Select
Layers
- Click on
Add a layer
- Select
Provide a layer version ARN
- Enter the ARN with the version from the previous Layer and click
Add
- Save the function
AWS CLI
Create the function: aws lambda create-function --function-name <name fo the function> --zip-file fileb://<path to the zip file> --handler function.handler --runtime provided --role <ARN of the IAM role>
Update the function: aws lambda update-function-configuration --function-name <name fo the function> --layers <full ARN of the Layer with version>
Once you have followed all the steps outlined above, you should have a ready to go Lambda Layer to be used in your functions. As you can see, AWS has made it pretty easy to add support for new languages and bundle reusable shared code. There are some limitations that you may run into along the way like the upper limit for the total size of Lambda unzipped and including all the layers is still 250MB. More on limits for Lambda - https://docs.aws.amazon.com/lambda/latest/dg/limits.html