DEV Community

Cover image for Going Serverless with Dart: AWS Lambda for Flutter Devs
Dinko Marinac
Dinko Marinac

Posted on • Originally published at dinkomarinac.dev

Going Serverless with Dart: AWS Lambda for Flutter Devs

Introduction

Welcome to part one of my series "Going Serverless with Dart". The point of this series is to show you how to write and deploy serverless logic (also known as cloud functions) to the most popular cloud providers: AWS and GCP.

Before we start, I want to briefly explain what serverless (computing) actually means. I really like this explanation from CloudFlare:

Serverless computing is a method of providing backend services on an as-used basis. A serverless provider allows users to write and deploy code without the hassle of worrying about the underlying infrastructure. Note that despite the name serverless, physical servers are still used but developers do not need to be aware of them.

In other words, serverless computing allows you to use only the backend parts you actually need, without worrying about deployment and maintenance. You pay for the components by usage and they auto-scale, but also introduces the risk of getting a huge bill from spiked usage like a DDOS attack.

Amazon offers its serverless components through a service called AWS (Amazon Web Services). Serverless (aka cloud) functions on AWS are called Lambdas.

They are developed based on a technology called Firecracker, which is a virtualization technology that uses Linux Kernel-based Virtual Machine (KVM) to manage micro virtual machines (one for each function).

Right now, you may be thinking: "Dinko, I don't care about Firecracker, whats that got to with Dart"?

You are about to find out.

Dart Lambda Custom Runtime

Lambdas officially supports typical backend languages like Node.js, Python, Go, Java, and Ruby. If you want to use another language, you can deploying it by using a custom runtime.

In February 2020, AWS introduced a custom runtime for Dart:

Dart runtime for AWS Lambda was first introduced in February 2020

You can access the runtime from pub.dev. You will notice one thing, the package has not been updated in the last 3 years (at the time of writing).

Thankfully, there is a fork of the package that provides a similar API but adapted to the null safety standards we are used to.

The runtime supports a lot of AWS services out of the box:

  • Application Load Balancer

  • Alexa

  • API Gateway

  • AppSync

  • Cloudwatch

  • Cognito

  • DynamoDB

  • Kinesis

  • S3

  • SQS

You can also register custom events.

To access all the different AWS services, you can use packages provided by Agilord. These packages are generated high-level APIs for AWS services.

With the custom runtime and all services accessible via packages, let's get to coding.

Writing your first lambda in Dart

Given all the possibilities with lambdas, I decided to show you how to write a lambda that reacts to a DynamoDB trigger. This is extremely useful for many use cases you might want to cover, especially if you are using AWS Amplify with Flutter, as it comes with DynamoDB by default.

ℹ️ You can find the full code on Github.

The event for DynamoDB update usually looks like this:

{
  "Records": [
    {
      "eventID": "c4ca4238a0b923820dcc509a6f75849b",
      "eventName": "INSERT",
      "eventVersion": "1.1",
      "eventSource": "aws:dynamodb",
      "awsRegion": "us-east-1",
      "dynamodb": {
        "Keys": {
          "id": {
            "S": "12345"
          }
        },
        "NewImage": {
          "id": {
            "S": "12345"
          },
          "name": {
            "S": "Buy groceries"
          }
        },
        "ApproximateCreationDateTime": 1428537600,
        "SequenceNumber": "4421584500000000017450439091",
        "SizeBytes": 26,
        "StreamViewType": "NEW_AND_OLD_IMAGES"
      },
      "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The data includes an array of changed events (Records) with each record containing:

  • eventID: Unique identifier for the event

  • eventName: Type of operation (INSERT, MODIFY, DELETE)

  • dynamodb: Contains the actual data changes

    • NewImage: The new state of the item
    • Keys: Primary key information

The function we will write will modify the name of the item when its created (inserted) in a DynamoDB table.

The first thing we must do is define a handler function that will handle our events. The FunctionHandler requires a function name and a callback with context which contains runtime information (like region, credentials, etc.) and raw event data.

The name you choose will be the same name you use for the deployment and management of the function later.

FunctionHandler get handleTodoCreation {
  return FunctionHandler(
    name: 'on-create-todo',
    action: (context, event) async {
      final dynamoDb = context.dynamoDb;

      try {
        final records = event['Records'] as List<dynamic>;

        await Future.forEach<dynamic>(records, (record) async {
          if (record['eventName'] == 'INSERT') {
            final newImage = record['dynamodb']['NewImage'];
            final todoId = newImage['id']['S'];
            final currentName = newImage['name']['S'];

            final modifiedName = 'Modified: $currentName';

            await dynamoDb.updateItem(
              tableName: 'todos',
              attributeUpdates: {
                'name': AttributeValueUpdate(
                  action: AttributeAction.put,
                  value: AttributeValue(s: modifiedName),
                ),
              },
              key: Key(hashKeyElement: AttributeValue(s: todoId)),
            );
          }
        });
      } catch (e) {
        print('Error processing DynamoDB stream event: $e');
      }
      return InvocationResult(requestId: context.requestId);
    },
  );
}
Enter fullscreen mode Exit fullscreen mode

The function iterates over the events and checks for INSERT events.

Then it extracts the the todo ID and current name. The name is modified by adding a "Modified: " prefix and then updated in the DynamoDB.

DynamoDB client is provided the aws_dynamodb_api package by Agilord. We can create the client using the credentials available from RuntimeContext. To make this easier to reuse, we can create an extension.

extension ContextExtensions on RuntimeContext {
  DynamoDB get dynamoDb => DynamoDB(
        region: region,
        credentials: AwsClientCredentials(
          accessKey: accessKey,
          secretKey: secretAccessKey,
          sessionToken: sessionToken,
        ),
      );
}
Enter fullscreen mode Exit fullscreen mode

Lastly, we must register the function handler using the invokeAwsLambdaRuntime method.

Future<void> main(List<String> args) async {
  await invokeAwsLambdaRuntime([handleTodoCreation]);
}
Enter fullscreen mode Exit fullscreen mode

We can use the AWS Console to test the function, but first, we must deploy it.

Deployment

There are many ways to deploy an AWS Lambda, but the two main ones are uploading a .zip file or Docker image or using Infrastructure-as-Code solutions like Serverless Framework, AWS CDK, or Terraform.

I'll show you how to deploy using a .zip file uploaded through the AWS Console since it also teaches you a little bit about which permissions you need to grant.

ℹ️ If you are curious about IaC deployment, I would recommend using AWS CDK. This article explains really well how to use it for deploying a DynamoDB triggered Lambda.

⚠️ When deploying Lambdas in production, make sure you follow the best security practices to avoid your data being compromised or leaving your endpoint vulnerable to attacks like DDOS. Common techniques include securing the Lambda with AWS API Gateway and giving it minimal permissions needed.

Before starting the deployment, make sure you have a DynamoDB table named todos. If you don't have have, go to DynamoDB in the AWS Console and create a new table named todos with a partition key Id. All other values can be default ones.

Enable event streaming in DynamoDB

First, we must enable DynamoDB Streams:

  1. Go to your todos table in the DynamoDB console

  2. Click on "Exports and streams" tab

  3. Under "DynamoDB stream details", click "Turn on"

  4. Select "New and old images" as the view type (this gives you both before/after states)

  5. Click "Turn on stream"

Create a .zip file

Next, we must create a zip file.

If you are using Mac or Windows, you must use Docker for this. If you are using Linux, you can just create a zip directly instead.

Make sure you have Docker installed. The easiest way is to install Docker Desktop.

I've prepared a runnable bash script which will take care of this step. Your project must also contain a Dockerfile which instructs docker how to build an image for AWS.

After running the script, you should have an output folder with boostrap and function.zip files inside. This function.zip is what you will upload to AWS.

Create a lambda and upload .zip

  1. Go to Lambda Console:
* Open AWS Console

* Search for "*Lambda*"

* Click "*Create function*"
Enter fullscreen mode Exit fullscreen mode
  1. Configure basic settings:
* Select "*Author from scratch*"

* Use the function name you specified in the handler (`on-create-todo` in the example)

* For Runtime, select "*Amazon Linux 2*"

* Architecture: select *x86\_64* or *arm64*, mine was arm64

* Click "*Create function*"
Enter fullscreen mode Exit fullscreen mode
  1. Upload your zip:
* In the Code tab of your function

* Click "*Upload from*" dropdown

* Select "*.zip file*"

* Upload your Dart zip file

* Click "*Save*"
Enter fullscreen mode Exit fullscreen mode
  1. Configure the handler:
* In the Runtime settings section

* Click "*Edit*"

* Set handler to `on-create-todo`

* Click "*Save*"
Enter fullscreen mode Exit fullscreen mode

Create a Dynamo DB trigger

In the Configuration tab, click "Add Trigger".

Then:

  1. Select "DynamoDB" as the trigger source

  2. Find the todos table and click the long link

  3. Make sure "Activate Trigger" is checked ☑️

Configure Environment Variables

In the Configuration tab, find the "Environment variables" sub-tab and click "Edit".

  1. Click "Add environment variable"

  2. The key should be: AWS_EXECUTION_ENV

  3. The value should be: AWS_Lambda_provided.al2

  4. Click "Save"

This is needed for the RuntimeContext of the lambda.

Grant permissions

Lastly, we want to grant permissions.

  1. Click on the Configuration tab of your Lambda

  2. Click on "Permissions"

  3. Click on the role name listed under "Execution role"

  4. Add the required policy:

* In the IAM role page, click "*Add permissions*" → "*Create inline policy*"

* Choose **JSON** and paste this policy:
Enter fullscreen mode Exit fullscreen mode
    ```json
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "dynamodb:GetRecords",
            "dynamodb:GetShardIterator",
            "dynamodb:DescribeStream",
            "dynamodb:ListStreams",
            "dynamodb:UpdateItem"
          ],
          "Resource": [
            "arn:aws:dynamodb:<region>:<account-id>:table/todos/stream/*",
            "arn:aws:dynamodb:<region>:<account-id>:table/todos"
          ]
        }
      ]
    }
    ```
Enter fullscreen mode Exit fullscreen mode
    You can find the region and the account id in the Lambda function ARN:

    ![How to find the region and account id for granting permissions for DynamoDB](https://cdn.hashnode.com/res/hashnode/image/upload/v1730719831761/cb2b6b43-9678-4d1a-9e9f-79a1100e1fd9.png)
Enter fullscreen mode Exit fullscreen mode
  1. Click "Next"

  2. Give it a name (e.g., TodosStreamReadAccess)

  3. Click "Create policy"

Testing the lambda

Firstly, you can test the lambda using the "Test" tab of of your Lambda and by providing the JSON specified at the beginning.

If something crashes or doesn't work, the AWS console error logs should be helpful. In case they are not, modify the lambda to output more errors and redeploy until you can solve the problem.

The real test is done by going the the DynamoDB.

  1. Click "View all tables"

  2. Click on the todos table

  3. Click "Explore table items"

  4. Click "Create item"

  5. Create an item similar to this:

    Example of creating a todo item in a DynamoDB console

  6. Click "Create Item"

  7. Find the new items

  8. Refresh the page

  9. The item with the same id (123456) should now have a Modified: prefix in the name

Congratulations 🎉, you have successfully written and deployed your first Dart Lambda on AWS!

Conclusion

Well, there you have it! You've just built and deployed your first Dart function on AWS Lambda. While it might seem like a lot of setup at first, you now have the power to write your cloud functions in the same language as your Flutter apps. Pretty neat, right?

Sure, using Dart for Lambda functions isn't as straightforward as using Python or Node.js, and you'll have to deal with a custom runtime. But hey, being able to share code between your app and cloud functions might just make it worth the extra effort.

If you are using AWS Amplify with your Flutter app, writing Lambdas using Dart now makes you a Full-stack Flutter developer. You have the ability to write serverless logic and your cross-platform app using Flutter and Dart.

In the next part of this series, we'll take what we've learned and see how it compares to building serverless functions on the Google Cloud Platform.

If you have found this useful, make sure to like and follow for more content like this. To know when the new articles are coming out, follow me on Twitter or LinkedIn.

Top comments (0)