Running Rust on AWS Lambda on ARM64
Tags: programming, aws, rust, serverless
Published: 2021-10-06 23:31:14

---
title: Running Rust on AWS Lambda on ARM64
published: true
tags: programming, AWS, Rust, serverless
canonical_url: https://www.ballpointcarrot.net/posts/rust-arm-lambdas/
---

> TL;DR: Rust on ARM64 Lambda: check out [my example repo](https://github.com/ballpointcarrot/lambda-rust-arm64)

I've been a hobbyist Rust developer for a while now, playing with random tools and mostly having false starts on projects that I'd like to build, but then the energy to do so wanes before I can complete them. Because of that, I always look for new applications for Rust to fit somewhere in my day-to-day, so that I can give myself some practice.

I also work very closely with AWS Serverless tooling in my daily work at [Stedi](https://stedi.com). Recently, AWS posted an update to the Lambda service that they were providing [Graviton2-based Lambda functions](https://aws.amazon.com/about-aws/whats-new/2021/09/better-price-performance-aws-lambda-functions-aws-graviton2-processor/) as part of the service, and I immediately thought to myself, "How would I do a Rust lambda that way?"

Lambda gives you the ability to create a ["Custom" runtime](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) - all you need to do is provide a executable that acts as a bootstrap for running the function (aptly named "bootstrap" in the runtime code). By building that as a rust binary with the correct architecture, we can run Rust code natively on the Lambda!

## Project Setup

I started with a vanilla cargo project, and added the following into the cargo.toml:

```toml
[[bin]]
name = "bootstrap"
path = "src/main.rs"

[dependencies]
lambda_runtime = "^0.4"
tokio = "^1"
serde = { version = "^1", features = ["derive"] }
serde_json = "^1"
```

The `[[bin]]` section allows us to specify the binary filename for what's built (and saves us renaming the file after compilation). The dependencies are all references and dependencies of [the AWS Rust runtime crates](https://github.com/awslabs/aws-lambda-rust-runtime) - that's where the code for the function we'll be using is pulled from.

we just have one file in the source directory, at `src/main.rs`:

```rust
use lambda_runtime::{handler_fn, Context, Error};
use serde_json::{json, Value};

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

async fn func(event: Value, _: Context) -> Result<Value, Error> {
    let first_name = event["firstName"].as_str().unwrap_or("world");
    Ok(json!({"message": format!("Hello, {}!", first_name)}))
}
```

For those less savvy with Rust code, this defines the 'bootstrap' runtime under `main()`, and calls the async function `func()` to act as the lambda function body. Our function responds with a message of "Hello, World!" in a JSON response by default, or can respond with a specific greeting when passed a "firstName" JSON value as part of its input.

Now, you could build this locally, but because of the way that the lambda function is called, local runs will panic:

```
± cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/bootstrap`
thread 'main' panicked at 'Missing AWS_LAMBDA_RUNTIME_API env var: NotPresent', /home/ckruse/.asdf/installs/rust/stable/registry/src/github.com-1ecc6299db9ec823/lambda_runtime-0.4.1/src/lib.rs:57:58
```

The immediate goal here is to run it in lambda, and not locally, so we'll save testing it out on our own for a later time.

## Cross-Compiling

The example that is provided in the AWS Rust runtime repository shows them using a [custom linker and rustup target](https://github.com/awslabs/aws-lambda-rust-runtime#aws-cli) in order to cross-compile for x86-based lambdas. I attempted to dive into how to get a arm64-based linker, but in the process of trying to figure out how to get that linker installed locally (no convenient packages for Ubuntu, sorry), I stumbled across a project that provides cross-compilation tooling for various platforms via Docker. That made my life a lot easier - Using the [rust-musl-cross](https://github.com/messense/rust-musl-cross) Docker container, all I have to do is wrap the call to `cargo build`, and it builds for the proper architecture:

```sh
#!/bin/sh

build_arch=aarch64-musl

docker run --rm -it -v "$(pwd)":/home/rust/src messense/rust-musl-cross:$build_arch $@
```

Armed with that (pardon the pun), I can now call `./musl-build cargo build --release`, and I get a resulting binary at `target/aarch64-unknown-linux-musl/release/bootstrap`.

The final step, as called out in the AWS Rust runtime docs, is to bundle that executable up in a Zip file, so that the Lambda service can construct what it needs to on its side. I've made the `package.sh` file in the repo to handle that and act as a basic build script (I'll probably convert this to a Makefile later when feeling energetic):

```sh
#!/bin/sh

mkdir -p dist

if [ ! -d target/aarch64-unknown-linux-musl ]; then
  ./musl-build cargo build --release
fi

# TODO: use vars for arch folder and binary name
cp target/aarch64-unknown-linux-musl/release/bootstrap dist/
cd dist && zip package.zip bootstrap && cd ..
```

Now, after running that command, I can grab the file at `dist/package.zip` and go take that to AWS Lambda to run:

With no arguments provided:

![Rust Lambda without name](https://www.ballpointcarrot.net/posts/rust-arm-lambdas/rust-arm-lambda-default.png)

With a name provided:

```json
{
  "firstName": "Christopher"
}
```

![Rust Lambda with firstName](https://www.ballpointcarrot.net/posts/rust-arm-lambdas/rust-arm-lambda-name.png)

## Conclusion

I hope you can use this to make a running start building out lambda function code on the new Graviton2-based Lambda runners. Let me know if this helped you out!