AWS Open Source Blog

Introducing a Dart runtime for AWS Lambda

Dart is a fast growing open source programming language, and powers some rapidly growing open source projects, such as Flutter. Thanks to custom AWS Lambda runtimes, you can run Dart in AWS Lambda. Writing your functions in Dart enables you to use your skills to develop mobile applications to create serverless backends. You also can share code between your application and your backend, and use the power of a statically typed language. In this post, I will explain how we support the language via custom Lambda runtimes, and how to create a Lambda function in Dart. I will share tips to help you along your builder’s journey.

What is Dart?

Dart is a popular open source, object-oriented, general-purpose programming language. Dart can be used to write a command-line script, mobile app, web app, or server-side app. It is a statically typed language with a sound type system that comes with a powerful package manager and developer tooling.

The language was one of the fastest growing in 2018-2019, which is not a big surprise because the very popular Flutter Mobile SDK is written in Dart. The SDK is one of the most trending open source projects on GitHub. Before diving in, you can learn more about the language and its features on the Dart site, and use DartPad to experiment with it.

Dart and the AWS Lambda custom runtime

With custom AWS Lambda runtimes, you can implement a runtime in any programming language. A custom Lambda runtime uses the AWS Lambda Runtime Interface when it runs. It defines an HTTP-based specification of the Lambda programming model that the custom runtime uses to serve invocation requests. Thanks to AWS Lambda Custom runtimes, you can also run Dart in Lambda with the recently published runtime for the Dart programming language. With the help of our runtime, you can also use it for serverless computing.

Getting started

In the following steps we are going to set up a lambda function using the Dart custom runtime and we are going to configure this to receive requests via an event source, in this example an application load balancer. This project will return a simple message back to the requestor.

For programs targeting devices (mobile, desktop, or server), Dart includes both a Dart VM with JIT (just-in-time) compilation and an ahead-of-time (AOT) compiler for producing the respective machine code. For programs targeting the web, it includes a Dart to JavaScript compiler. AWS Lambda is a server device target and the runtime project uses the AOT compiler to produce the machine code for the target.

Dart 2.6 introduced the dart2native command-line tool, which makes producing native machine code easier. It compiles a Dart program to the needed native x64 machine code, plus a small Dart runtime that handles type checking and garbage collection. The runtime project uses the tool to compile a Lambda function and the runtime to the native machine code for AWS Lambda. The created binary is executable in the standard Lambda execution environment that custom runtimes use.

AWS Lambda can run a created function in response to multiple native events, such as an HTTP(s) request via an Application Load Balancer (ALB). ALBs can trigger the function to process the request.

illustration of ALBs can trigger the function to process the request.

The event that invokes the function contains the request metadata and body. In this article, I will explain how to build the function that processes the event. The function will return an HTTP response as a result of the processing.

Prerequisites

You need a source code editor to create the source file for your Lambda function and the package specification for the package manager. Visual Studio Code plus the Dart Code plugin is a great choice, if you are a fan of open source and want to start creating your function quickly. The plugin requires you to get the Dart SDK for most of its features.

Creating, building, and deploying a Dart function

To start, create a folder for your Dart package and add package specification to it. You will need the specification to use the pub package manager, which manages the dependencies and environment of your Dart program with the information from the specification.

The following command creates the folder for the package:

bash $ > mkdir my_lambda && cd $_ 

In the folder, create a pubspec.yaml file with the following content:

name: my_lambda
environment:
  sdk: ">=2.6.0 <3.0.0"
dependencies:
  aws_lambda_dart_runtime:
    git: https://github.com/awslabs/aws-lambda-dart-runtime.git
dev_dependencies:
  build_runner:

This is the package specification. It specifies the Dart runtime as a dependency for your program. The package is fetched from the master branch of the Dart Lambda runtime repository until AWS becomes a publisher on pub.dev, a repository of publicly available packages.

Next, create a main.dart file and open it in your preferred source code editor. Copy the following Dart program into the file:

import 'package:aws_lambda_dart_runtime/aws_lambda_dart_runtime.dart';

void main() async {
  /// This demo's handling an ALB request.
  final Handler<AwsALBEvent> helloALB = (context, event) async {
    final response = '''
<html>
<header><title>My Lambda Function</title></header>
<body>
Success! I created my first Dart Lambda function.
</body>
</html>
''';
    /// Returns the response to the ALB.
    return InvocationResult(
        context.requestId, AwsALBResponse.fromString(response));
  };
  /// The Runtime is a singleton.
  /// You can define the handlers as you wish.
  Runtime()
    ..registerHandler<AwsALBEvent>("hello.ALB", helloALB)
    ..invoke();
}

The program registers the helloALB function as hello.ALB handler in the runtime. A handler is an identifier for a specific function in your code that AWS Lambda can invoke when it runs your function. An invocation request contains the handler the runtime should execute, and the runtime maps this handler to the real function, which it then runs. The hello.ALB handler in the program is typed with an AwsALBEvent event. Thus, the runtime expects to receive an Application Load Balancer event when AWS Lambda invokes the hello.ALB handler. The AwsALBEvent is a convenient wrapper in the Dart runtime to extract the JSON from an Application Load Balancer event into a Dart data structure. Learn about all available events from the Dart runtime for AWS Lambda documentation.

The next step, which includes a caveat, is compiling the function. The dart2native tool has no cross-platform compiler support (see #28617). This is important if you are not on the Linux x86-64 platform. Binaries that are not compiled on this platform cannot be executed on AWS Lambda. You will learn to compile the function on other platforms using Docker.

Compiling with Docker

First, you must install Docker on your operating system if it is not already installed. If you do not know about Docker, read “What is Docker?” To compile the function, you will use the official google/dart Docker container.

Next, proceed step by step to produce the binary. These steps can be automated if you use continuous integration. To learn about an example build.sh script, check the script in the example folder of the Dart Lambda runtime project.

First, mount the package in the container and get an interactive shell to it:

bash $ > docker run -v $PWD:/app -w /app -it google/dart /bin/bash

The command enters the working directory at /app. Fetch the needed dependencies via pub get. Then you are ready to compile the Dart program to machine code, plus the Dart runtime:

bash $ > dart2native main.dart -o bootstrap

Running ls -lh should have the following output. The bootstrap file is the compiled main.dart program; it is intentionally named bootstrap. The Lambda runtime will execute any file named bootstrap when it is launching the function (see the “Custom AWS Lambda Runtimes” documentation):

-rwxr-xr-x 1 root root 8.0M Feb 3 10:03 bootstrap
-rw-r--r-- 1 root root 665 Feb 3 09:55 main.dart
-rw-r--r-- 1 root root 9.2K Feb 3 10:02 pubspec.lock
-rw-r--r-- 1 root root 193 Feb 3 09:25 pubspec.yaml

In the final step, exit the interactive shell in the container with exit.

Compiling on Linux

If you are running on Linux x86-64, you do not have to use a Docker container; however, you must install Dart to your development environment:

bash $ > pub get && dart2native  main.dart -o bootstrap

These commands fetch the dependencies and compile the program in main.dart to a bootstrap binary, which is executable in a custom AWS Lambda. You will see a message that confirms that the binary has successfully generated. The binary is named bootstrap, because the custom runtime executes any file with that name when it is launching the function.

Building the function

The bootstrap binary is ready for the final step before you can deploy it: creating a .zip file of it via zip -j lambda.zip bootstrap. The lambda.zip is the package used to deploy the function to AWS Lambda.

Deploying the function on AWS Lambda

The function you created is intended to be invoked by an Application Load Balancer HTTP(S) request (refer to the article “Lambda functions as targets for Application Load Balancers“). The function expects the runtime to return an ALB invocation event, which it wraps in a Dart object.

Next, navigate to the AWS Lambda console and click create function.

Screen capture of the AWS Lambda conslue and where to click create function

Create the function from scratch and name it myLambda. Next select Provide your own bootstrap from the Runtime dropdown. Create a new execution role for the Lambda and name it myLambdaExecutionRole, but you also can use your existing role. Finally, create the function by clicking create function. A message says that you have successfully created your function and you will be redirected to the detailed page of the function.

In the next step, deploy the Lambda function. Under the function code section, select to upload a .zip file. Rename the Handler to hello.ALB, which is the name of the handler you registered in our main.dart. Then click to upload it under the function package and select the lambda.zip from the my_lambda folder. Hit save to upload your package.

Screenshot that shows where to click to upload it under the function package and select the lambda.zip from the my_lambda folder, and hitting save to upload your package.

You have successfully deployed our Lambda function.

Testing the function

The next step is to test that it works and that the handler is invoked by the runtime. Thus, you must create a test event and invoke the function.
To do so, click on the select a test event and select the configure test events option.

screenshot for clicking on the select a test event and selecting the configure test events option

Call the test event myLambdaALBTestEvent and use the following JSON from the docs for the event body:

{
    "requestContext": {
        "elb": {
            "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a"
        }
    },
    "httpMethod": "GET",
    "path": "/lambda",
    "queryStringParameters": {
        "query": "1234ABCD"
    },
    "headers": {
        "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
        "accept-encoding": "gzip",
        "accept-language": "en-US,en;q=0.9",
        "connection": "keep-alive",
        "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com",
        "upgrade-insecure-requests": "1",
        "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
        "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476",
        "x-forwarded-for": "72.12.164.125",
        "x-forwarded-port": "80",
        "x-forwarded-proto": "http",
        "x-imforwards": "20"
    },
    "body": "",
    "isBase64Encoded": false
}

screenshot from creating a new test event

Clicking create creates the test event. The myLambdaALBTestEvent should be created and show up as pre-selected in the dropdown menu.

Clicking create creates the test event. The myLambdaALBTestEvent should be created and show up as pre-selected in the dropdown menu.

You are ready to test the event. Click on the test button to trigger the test event. Expanding the Details of the execution displays a successful response to the request, containing the example HTTP response body.

Click on the test button to trigger the test event. Expanding the Details of the execution displays a successful response to the request, containing the example HTTP response body.

Now that you have successfully tested your function, you are ready to connect the function to a real ALB. You can read more about this in the “Lambda functions as targets for Application Load Balancers” article.

Custom event

The Dart runtime supports custom events. These are events that are not native to the AWS platform and its services, but are created by customers. To create a custom event, you create a class for it and register it with the instance of the runtime. The runtime is implemented as a singleton to be flexibly used in your code structure and to avoid creating multiple instances.

Create a MyCustomEvent class for a new custom event. The class only needs to implement a factory MyCustomEvent.fromJson(Map<String, dynamic> json) method. This method is called by the runtime when it later looks up a MyCustomEvent of an event handler and creates the event from the JSON representation of the event:

class MyCustomEvent {
  factory MyCustomEvent.fromJson(Map<String, dynamic> json) =>
      MyCustomEvent(json);
  const MyCustomEvent();
}
 
Register the MyCustomEvent with the runtime as follows:
Runtime()
    ..registerEvent((Map<String, dynamic> json) => MyCustomEvent.from(json);
The full example of a MyCustomEvent looks like this:
import 'package:aws_lambda_dart_runtime/aws_lambda_dart_runtime.dart';
class MyCustomEvent {
  factory MyCustomEvent.fromJson(Map<String, dynamic> json) =>
      MyCustomEvent(json);
  const MyCustomEvent();
}
void main() async {
  final Handler successHandler =
      (context, event) async {
    return InvocationResult(context.requestId, "SUCCESS");
  };
  Runtime()
    ..registerEvent((Map<String, dynamic> json) => MyCustomEvent.from(json))
    ..registerHandler("doesnt.matter", successHandler)
    ..invoke();
}
.

A handler function is passed in a Context object. The object contains information about the invocation from the Lambda runtime interface, such as the requestId, region, or functionName. Learn more about custom events and the full API in the Dart runtime for AWS Lambda documentation.

Conclusion

The Dart Lambda runtime is still in an early stage. The maintainers want to get your feedback for its evolution. In particular, they want to hear about workloads that you experiment with, or workloads that they should know about and support. You can visit the aws-lambda-dart-runtime Github repository to share your experience or to submit issues.

Feature image via Pixabay.

TAGS:
Sebastian Doell

Sebastian Doell

Sebastian is a solutions architect at AWS. Sebastian helps startups to execute on their ideas at speed and at scale. He maintains a number of open source projects and advocates Dart and Flutter.