All articles

-

AWS Lambda + Rust

devopsrustawsbackend
08 Nov 2020
-

TL;DR: There's an example repo here for those who want to skip the story mode

Rust piqued my interest when I found out it consistently ranked first in the StackOverflow's annual developer survey for the world's most loved programming language. Here's the 2020 survey, but it also holds the first position for 2019, 2018, 2017, and 2016.

It turns out it's as awesome as they say and now I'm in that particular moment in the hype phase when I try to do everything in Rust. I know that's a terrible idea and I strongly advise against it: pick the language that has the strongest support (aka libraries, community) for the problem you're trying to solve. Doing ML in Rust when Python is de facto standard is not such a great idea.

Anyways, I figured I can make an exception and since I'm not that excited about any of the popular backend languages, I started to experiment in that direction.

Running a Rust HTTP server using Rocket is really easy and well documented, however, if you plan to go serverless, there's still a lot of uncharted territories.

For AWS Lambda, there are a couple of resources out there, but many are outdated or somehow incomplete.

Here are the main steps we'll have to follow:

  • implement the lambda handlers
  • (cross)compile our code for the Amazon Linux platform (x86, 64bit)
  • build each lambda as a standalone binary
  • configure AWS Lambda for deployment
  • deploy & enjoy

So, let's get started!

# create a new crate
cargo new rust_aws --bin
# delete the main.rs, we'll be using a binary for each lambda
cd rust_aws && rm src/main.rs
# these are the two lambdas we're going implement
touch src/comment.rs
touch src/contact.rs

Next, our dependencies in Cargo.toml

[package]
name = "your_proj_name"
version = "0.1.0"
authors = ["You <you@example.com>"]
edition = "2018"

[dependencies]
lambda_runtime = "0.2.1"
lambda_http = "0.1.1"
tokio = { version = "^0.3", features = ["full"] }

[[bin]]
name = "comment"
path = "src/comment.rs"

[[bin]]
name = "contact"
path = "src/contact.rs"

A couple of things to mention here. First, we have the lambda_runtime and lambda_http crates which are responsible for communicating with the Lambda API. This usually means running the setup code, fetching the handler name from an environment variable, and passing events to our code. You can find out more about how custom runtimes work here.

Although lambdas are stateless, AWS can run our binaries and send multiple events to the same process, as long as the process doesn't exit. This requires an event loop: a fancy way to handle asynchronous I/O and scheduling. We use tokio for that.

Finally, we declared 2 different binaries, one named comment, the other contact and each will be deployed as a standalone lambda function

Next up, compilation. Unless you're on an x86, 64bit Linux machine, you'll have to cross-compile your code. To do so, we need the correct toolchain:

# adds the x86 64 target to the toolchain
rustup target add x86_64-unknown-linux-musl
# installs the x86 64 toolchain on macOS (for Windows, you can probably do it with cygwin-gcc-linux, but I haven't tried it out)
brew install FiloSottile/musl-cross/musl-cross

Lastly, we need to let cargo know we're cross-compiling: add the following in ./.cargo/config.toml

[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"

Now we're ready to compile. cargo build --target x86_64-unknown-linux-musl to test it out.

The next thing we need to do is to configure SAM. I'll assume you're already familiar with SAM and focus only on the critical section for our case. You can have a look at the full template.yml in the example repository. Also, skip sam init since there is no Rust template available anyhow (to my knowledge) and simply start with the template.yml file and build your own directory structure.

Let's go through the one of the lambdas' definition:

# template.yml
Resources:
    Comment:
    Type: AWS::Serverless::Function
    Properties:
    FunctionName: Comment
    Handler: doesnt.matter.the.runtime.is.custom
    Runtime: provided
    MemorySize: 128
    Timeout: 10
    CodeUri: .
    Description: 
    Policies:
        - AWSLambdaBasicExecutionRole
    Events:
    comment:
    Type: Api
    Properties:
    Path: /comment
    Method: post

This tells SAM to create a lambda serverless function named Comment, with a custom runtime (handled by or Rust lambda_runtime) and expose it as a REST API resource at /comment.

We're almost done. One last (important) thing: when we build and deploy our lambdas with sam build && sam deploy --guided SAM will look for a Makefile since it doesn't know how to build our project by itself.

touch Makefile
build-Comment:
    cargo build --bin comment --release --target x86_64-unknown-linux-musl
    cp ./target/x86_64-unknown-linux-musl/release/comment $(ARTIFACTS_DIR)/bootstrap

build-Contact:
    cargo build --bin contact --release --target x86_64-unknown-linux-musl
    cp ./target/x86_64-unknown-linux-musl/release/contact $(ARTIFACTS_DIR)/bootstrap

The way this works is straightforward, you need to add a target for each lambda name and prefix it with build-. That's it. SAM will invoke them as needed. Each target builds the respective binary (--bin contact) and copies it in the artifacts directory, where it will be zipped and sent to the AWS servers for deployment.

And that's it. We're done. Have fun with your new Rust-powered AWS lambdas!