{"id":1593,"date":"2020-08-10T10:15:50","date_gmt":"2020-08-10T08:15:50","guid":{"rendered":"https:\/\/blog.besharp.it\/?p=1593"},"modified":"2021-03-18T16:20:34","modified_gmt":"2021-03-18T15:20:34","slug":"how-to-run-any-programming-language-on-aws-lambda-custom-runtimes","status":"publish","type":"post","link":"https:\/\/blog.besharp.it\/how-to-run-any-programming-language-on-aws-lambda-custom-runtimes\/","title":{"rendered":"How to run any programming language on AWS Lambda: Custom Runtimes."},"content":{"rendered":"
AWS Lambda Functions as a Service (FaaS) have quickly become the Swiss Army knife of the AWS cloud DevOps since it is possible to use them for a plethora of different tasks. From the backend of a web application to the ingestion of an AWS IoT application, from simple infrastructure automation to the real-time analysis of messages delivered through AWS SQS or AWS Kinesis. Furthermore, they are cheap, versatile, scalable, reasonably priced, and trivial to set up and maintain.\u00a0<\/span><\/p>\n To make them more attractive for developers and devops AWS offers an increasing number of Lambda runtimes, which allow you to write your code in different versions of several programming languages. At the moment of this writing, AWS Lambda natively supports Java, Go, PowerShell, Node.js, C#, Python, and Ruby.\u00a0<\/span><\/p>\n However, there are lots of other programming languages out there that you may want to use in your Lambda function to migrate to AWS Lambda existing applications currently deployed on-premises or on EC2 instances. Since rewriting a current code is often infeasible due to lack of time or missing libraries and functionality AWS recently began to offer a new possibility:\u00a0<\/span>you can create a custom Runtime for Lambda and use the programming language you prefer!<\/span><\/strong><\/p>\n A Lambda function with a custom runtime environment differs from a normal lambda function because it not only contains the code the Lambda will run when the function is invoked but also all the compiled libraries needed to make the code run and – if the language you choose is interpreted like PHP or just in time compiled like Julia – you also need to include the binary interpreter. For most programming languages, a custom runtime prepared by third parties is usually available on github and is often either usable directly or a good base for a custom solution.<\/span><\/p>\n In the following section, however, we will describe how to create a generic runtime and present two examples created by AWS:\u00a0<\/span>bash and C++<\/span><\/strong>. Finally, we will compare the lambda response time of the custom runtimes with the one of a native runtime (Python 3.8). Creating a custom runtime also allows us to understand how the lambda service works under the hood, which is often very useful for solving problems that may arise with lambda functions (e.g., cold starts).<\/span><\/p>\n AWS Lambda consists of two main parts: the\u00a0<\/span>Lambda\u00a0<\/span>service<\/span><\/em><\/strong>\u00a0which manages the execution requests, and the\u00a0<\/span>Amazon Linux micro virtual machines<\/span><\/strong>\u00a0provisioned using AWS Firecracker, which actually runs the code. A Firecracker VM is started the first time a given Lambda function receives an execution request (the so-called \u201cCold Start\u201d), and as soon as the VM starts, it begins to poll the Lambda service for messages. When the VM receives a message, it runs your function code handler, passing the received message JSON to the function as the event object.\u00a0<\/span><\/p>\n Thus every time the Lambda service receives a Lambda execution request, it checks if there is a Firecracker microVM available to manage the execution request. If so, it delivers the message to the VM to be executed. In contrast, if no available Firecracker VM is found, it starts a new VM to manage the message.\u00a0<\/span><\/p>\n Each VM executes one message at a time, so if a lot of concurrent requests are sent to the Lambda service, for example due to a traffic spike received by an API gateway, several new Firecracker VMs will be started to manage the requests and the average latency of the requests will be higher since each VM takes roughly a second to start.\u00a0<\/span><\/p>\n In a lambda function using a native runtime, you do not need to worry about how your function will poll for messages from the lambda service and send execution reports back to the lambda service; the native runtime will take care of that for you. However, this is not the case for a custom runtime. When a Lambda function is created with a custom runtime, AWS Lambda Service will start a basic AmazonLinux VM without any installed libraries except for vanilla bash and a few basic unix commands (e.g., ls, curl). Differently from a normal Lambda, in addition to your code and external libraries, you\u2019ll also need to include in the deployment package a script or executable called \u201cbootstrap\u201d that will manage the interaction between the function VM and the Lambda service. AWS Lambda Service exposes a simple HTTP interface for runtimes to receive invocation events from Lambda and send response data back.<\/span><\/p>\n The bootstrap program needs to perform the following tasks:<\/span><\/p>\n The bootstrap script\/executable and other language level libraries and interpreters (e.g., PHP interpreter) can be included in a dedicated lambda layer to generate a generic and portable custom runtime used on several Lambda Functions.<\/span><\/p>\n The easiest way to get going with custom runtime is through the AWS Console: from the Lambda Service Dashboard select Create Lambda and in the runtime section select Custom Runtime with Use Default bootstrap and click Create Function<\/span><\/p>\n <\/p>\n Using these default settings, the Lamba service will create a basic Bash Lambda with a default bootstrap script. Let\u2019s take a look at the previously generated bootstrap script:<\/p>\n <\/p>\n <\/p>\n Let\u2019s quickly review what this script does:\u00a0<\/span><\/p>\n <\/p>\n It should be noted that in a real complete custom runtime you\u2019ll also need to manage errors and exceptions by calling the error invocation by calling the Invocation error API (<\/span>\/runtime\/invocation\/AwsRequestId\/error<\/span>) when an exception is raised by the Handler method:<\/span><\/p>\n Let\u2019s now move to a more complicated example: calculating the first n digits of pi in Lambda using a trivial and inefficient version of the Spigot Algorithm.\u00a0<\/span><\/p>\n Running complicated calculations in Lambda function is often non-trivial both due to the lacking computational power reserved to the Firecracker VM (at least the low memory ones, memory and CPU scales proportionally in lambda function) and to the nature of the languages of native runtimes which are not too suited to high-performance computations (except Go). Conversely, C++ has a long and successful history in high-performance computing with lots of libraries available, from arbitrary precision Arithmetic to Matrix computations, from Fluid Dynamics to Particle Collisions.<\/span><\/p>\n Furthermore, this language is a first-class citizen in AWS with a full AWS SDK and a Lambda Runtimes builder developed and maintained directly by AWS.<\/span><\/p>\n To create our example, we can just clone the AWS\u00a0<\/span>git repository\u00a0<\/span><\/a>of the Lambda runtime builder and build the library using the commands (on UNIX):<\/span><\/p>\n <\/p>\n After that let\u2019s move to the Api Gateway Example in the examples folder and change the code in the main.cpp with:<\/p>\nHow does Lambda Works?<\/h2>\n
\n
\n
\n
\n
\n
\n
\n
How to create a Bash Lambda with custom runtimes<\/h2>\n
#!\/bin\/sh\r\nset -euo pipefail\r\n\r\n# Handler format: .\r\n#\r\n# The script file .sh must be located at the root of your\r\n# function's deployment package, alongside this bootstrap executable.\r\nsource $(dirname \"$0\")\/\"$(echo $_HANDLER | cut -d. -f1).sh\"\r\n\r\nwhile true\r\ndo\r\n # Request the next event from the Lambda runtime\r\n HEADERS=\"$(mktemp)\"\r\n EVENT_DATA=$(curl -v -sS -LD \"$HEADERS\" -X GET \"http:\/\/${AWS_LAMBDA_RUNTIME_API}\/2018-06-01\/runtime\/invocation\/next\")\r\n INVOCATION_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id \"$HEADERS\" | tr -d '[:space:]' | cut -d: -f2)\r\n\r\n # Execute the handler function from the script\r\n RESPONSE=$($(echo \"$_HANDLER\" | cut -d. -f2) \"$EVENT_DATA\")\r\n\r\n # Send the response to Lambda runtime\r\n curl -v -sS -X POST \"http:\/\/${AWS_LAMBDA_RUNTIME_API}\/2018-06-01\/runtime\/invocation\/$INVOCATION_ID\/response\" -d \"$RESPONSE\"\r\ndone\r\n<\/pre>\n
\n
\n
REQUEST_ID=156cb537-e2d4-11e8-9b34-d36013741fb9\r\nERROR=\"{\\\"errorMessage\\\" : \\\"Error parsing event data.\\\", \\\"errorType\\\" : \\\"InvalidEventDataException\\\"}\"\r\ncurl -X POST \"http:\/\/${AWS_LAMBDA_RUNTIME_API}\/2018-06-01\/runtime\/invocation\/$REQUEST_ID\/error\" -d \"$ERROR\" --header \"Lambda-Runtime-Function-Error-Type: Unhandled\"\r\n<\/pre>\n
A simple c++ example: Calculating the first n digits of pi in Lambda<\/h2>\n
$ git clone https:\/\/github.com\/awslabs\/aws-lambda-cpp.git\r\n$ cd aws-lambda-cpp\r\n$ mkdir build\r\n$ cd build\r\n$ cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=~\/lambda-install\r\n$ make && make install\r\n<\/pre>\n
#include <aws\/lambda-runtime\/runtime.h>\r\n#include <aws\/core\/utils\/json\/JsonSerializer.h>\r\n#include <aws\/core\/utils\/memory\/stl\/SimpleStringStream.h>\r\n#include \r\n\r\nusing namespace aws::lambda_runtime;\r\n\r\nvoid pi_digits(int x, Aws::SimpleStringStream &s)\r\n{\r\n long x_initial = x;\r\n unsigned long long k = 2;\r\n unsigned long long a = 4;\r\n unsigned long long b = 1;\r\n unsigned long long a1 = 12;\r\n unsigned long long b1 = 4;\r\n while (x > 0) {\r\n unsigned long long p = k * k;\r\n unsigned long long q = 2 * k +1;\r\n k = k + 1;\r\n\r\n unsigned long long a1old = a1;\r\n unsigned long long b1old = b1;\r\n\r\n a1 = p * a + q * a1;\r\n b1 = p * b + q * b1;\r\n a = a1old;\r\n b = b1old;\r\n\r\n long double d = a \/ b;\r\n long double d1 = a1 \/ b1;\r\n\r\n while ((d == d1) && (x > 0)) {\r\n s << static_cast(floor(d));\r\n if (x_initial == x) {\r\n s << \".\";\r\n }\r\n x -= 1;\r\n a = 10 * (a % b);\r\n a1 = 10 * (a1 % b1);\r\n d = a \/ b;\r\n d1 = a1 \/ b1;\r\n }\r\n }\r\n}\r\n\r\n\r\ninvocation_response my_handler(invocation_request const& request)\r\n{\r\n using namespace Aws::Utils::Json;\r\n\r\n JsonValue json(request.payload);\r\n if (!json.WasParseSuccessful()) {\r\n return invocation_response::failure(\"Failed to parse input JSON\", \"InvalidJSON\");\r\n }\r\n\r\n auto v = json.View();\r\n Aws::SimpleStringStream ss;\r\n\/\/\r\n\/\/ pi_digits(10, ss);\r\n if (v.ValueExists(\"queryStringParameters\")) {\r\n auto query_params = v.GetObject(\"queryStringParameters\");\r\n pi_digits((query_params.ValueExists(\"number\") && query_params.GetObject(\"number\").IsString() ? stol(query_params.GetString(\"number\")) : 10), ss);\r\n }\r\n\r\n JsonValue resp;\r\n resp.WithString(\"message\", ss.str());\r\n\r\n return invocation_response::success(resp.View().WriteCompact(), \"application\/json\");\r\n}\r\n\r\nint main()\r\n{\r\n run_handler(my_handler);\r\n return 0;\r\n}\r\n<\/pre>\n