Rust with Serverless Framework: Introduction

Introduction on how to deploy simple Lambda function in AWS using Rust programming language.

Rust with Serverless Framework: Introduction
Photo by Zdeněk Macháček / Unsplash

The Serverless framework is our go-to environment for serverless applications. Particularly, we are focused on developing applications that run on AWS. Thus, in this post, we will mostly focus on AWS Lambda-specific topics.

AWS has taken great efforts on bringing Rust into a first-class citizen in Lambda. They have published their Rust runtime environment as open-source on Github https://github.com/awslabs/aws-lambda-rust-runtime. It is easy to verify that it is under active development. This is good news as Rust has not yet gathered a whole lot of spaces from the serverless world, but it is on its way.

Serverless has also a plugin to support applications made with Rust https://www.serverless.com/plugins/serverless-rust. This should bring us all the tools to start developing Serverless applications in Rust.

Note that the Serverless framework plugin is specifically designed for AWS Lambda.

Why Rust?

Serverless applications are mostly done in higher-level languages such as JavaScript, TypeScript, and Python. It is easy to see why. They have a fantastic ecosystem behind them that has readily available libraries for most tasks.

So why would we want to go with a lower-level language that does not have as wide a range of libraries accessible?

Performance is the most obvious answer. Compiled languages, such as Rust, have inherent performance benefits attached to them. In serverless applications, this usually means faster runtime and lower memory usage, which translates into better service and lower costs.  

Preliminaries

We're assuming that you have a Rust development environment installed. The default behavior of serverless-rust plugin is to use dockerized builds. However, I found it somewhat tedious to use and reverted to the dockerless builds. In any case, the dockerless build environment is supposed to be enabled by default in the future.

Furthermore, you should have basic knowledge of the serverless framework, and know your way around the basic workflow.

To keep things nice and tidy, AWS Rust runtime relies on musl libc. So you will probably need a cross compiler for musl. Luckily, this is easy to do with rustup. Just run

rustup target add x86_64-unknown-linux-musl
Rust cross compiler installation

Simple handler source

We attempt to use a minimal setup for our first project without any unnecessary bloat that we all tend to gather in our projects as they progress. Let's start by having a look at a super simple handler code in Rust. We have taken the example handler from the AWS Rust runtime examples. Add the following in src/main.rs.

use lambda_runtime::{service_fn, LambdaEvent, Error};
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> Result<(), Error> {
    lambda_runtime::run(service_fn(func)).await?;
    Ok(())
}

async fn func(event: LambdaEvent<Value>) -> Result<Value, Error> {
    let (event, _context) = event.into_parts();
    let first_name = event["firstName"].as_str().unwrap_or("world");

    Ok(json!({ "message": format!("Hello, {}!", first_name) }))
}
src/main.rs

We use service_fn to declare the handler function, in this case, named func. AWS runtime runs this function and passes the LambdaEvent as a parameter. The event contains all information passed to the event handler.

Events are split into actual event body and context. Since this is just a simple hander, we ignore the context and focus on the event body. The event body is assumed to contain property firstName. If it is not present, we populate it with a string world. Then we return a JSON response with a property message containing the string Hello {firstName}!.  

Serverless configuration  

Now that we have set up our simple handler code. We can configure our serverless environment around it.  

Let's start with package.json . We'll only install the serverless base and rust dependencies.  

{
  "devDependencies": {
    "serverless": "^1.74.1",
    "serverless-rust": "^0.3.8"
  },
  "name": "serverless-with-rust"
}
package.json

Install all necessary packages in the development environment

yarn install 

Now we are ready to install the necessary Rust packages. We need the lambda dependencies and, additionally, we use tokio to implement our unit tests.

[package]
name = "serverless-with-rust"
version = "0.1.0"
edition = "2018"

[dependencies]
tokio = { version = "1.17.0", features = ["macros"] }
serde_json = "1.0"
lambda_runtime = { version = "0.5.1" }
lambda_http = { version = "0.5.1" }
Cargo.toml

You can try cargo build to check that you can build the dependencies. Note that running cargo directly will use your system's default Rust build environment, which is not the same that is used by serverless to build the packages.

Next, we move on to configuring the actual serverless environment. We initialize our serverless.yml template with the following

service: serverless-with-rust
provider:
  name: aws
  runtime: rust
  memorySize: 128
  stage: dev
  region: eu-central-1

package:
  individually: true

custom:
  rust:
    dockerless: true

plugins:
  - serverless-rust

functions:
  func:
    handler: "serverless-with-rust"

Most of this is pretty much self-explanatory if you know your way around serverless workflow. Not that we set up the dockerless build by setting custom:rust:dockerless to true. Note also that the handler name is the package name defined in Cargo.toml.

Test the build by running

npx serverless package

If the packaging succeeds without any hiccups, we can safely assume that the build environment is working.

You are now ready to deploy the app by running

npx serverless deploy

You now also invoke the default func lambda by running

npx serverless invoke -f func -d '{"firstName":"me"}'

Congratulations! You have successfully deployed your first rust applications using the serverless framework.

Bonus: unit testing

Here's how to add simple unit testing for your simple handler function. We just test different event bodies and check for the appropriate response.  

use lambda_runtime::{service_fn, LambdaEvent, Error};
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> Result<(), Error> {
    lambda_runtime::run(service_fn(func)).await?;
    Ok(())
}

async fn func(event: LambdaEvent<Value>) -> Result<Value, Error> {
    let (event, _context) = event.into_parts();
    let first_name = event["firstName"].as_str().unwrap_or("world");

    Ok(json!({ "message": format!("Hello, {}!", first_name) }))
}

#[cfg(test)]
mod tests {
    use super::*;
    use lambda_runtime::Context;

    #[tokio::test]
    async fn handler_default_parameter() {
        let event = LambdaEvent::new(json!({}), Context::default());
        assert_eq!(
            func(event.clone())
                .await
                .expect("expected Ok(_) value"),
            json!({"message": "Hello, world!"})
        )
    }

    #[tokio::test]
    async fn handler_custom_parameter() {
        let event = LambdaEvent::new(json!({"firstName": "Jake"}), Context::default());
        assert_eq!(
            func(event.clone())
                .await
                .expect("expected Ok(_) value"),
            json!({"message": "Hello, Jake!"})
        )
    }
}
src/main.rs

You can execute the unit tests as you normally would by running

cargo test

Further reading

Check these resources for further reading on how to use Rust with the Serverless framework and AWS Lambdas.

Rust Runtime for AWS Lambda | Amazon Web Services
中文版 AWS Lambda, which makes it easy for developers to run code for virtually any type of application or backend service with zero administration, has just announced the Runtime APIs. The Runtime APIs define an HTTP-based specification of the Lambda programming model which can be implemented in any p…
GitHub - scicoding/serverless-rust-introduction: Introduction on how to deploy simple Lambda function in AWS using Rust programming language.
Introduction on how to deploy simple Lambda function in AWS using Rust programming language. - GitHub - scicoding/serverless-rust-introduction: Introduction on how to deploy simple Lambda function ...