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!