Misadventures Playing in Rust on AWS Lambda
Tags: aws, rust, beginners, m1
Published: 2022-02-19 00:54:09

That feeling of embarking on something new.

<img width="100%" style="width:100%" src="https://media.giphy.com/media/VGG8UY1nEl66Y/giphy.gif">
Not pictured is the wall I ran into.

I am a Python developer. My life has been lived within the safe confines of an interpreter and devoid of realities of compilers and platform architectures. Life is sweet. Despite this, I have found myself being lured by the prospects and promises of what compiled languages can bring: ludicrous speed.

Since 2020 it has been clear to me that AWS has decided on Rust as their Go. The release of the official [AWS SDK for Rust](https://github.com/awslabs/aws-sdk-rust), and a maturing [Rust runtime for Lambda](https://github.com/awslabs/aws-lambda-rust-runtime), signaled that it might be time for me to explore what developing my functions in this language might look like.

## The Setup

Up until now I've heard that Rust can be difficult, but I can safely say that getting started with Rust itself is _not_ difficult. Following the instructions in the [Rust Book](https://doc.rust-lang.org/book/) resulted in my ability to write and compile a program without a single hitch.

Where I really ran into trouble was trying to use the Rust runtime and AWS SDK and deploy a Lambda Function. I am writing code on an Apple Silicon MacBook Pro (M1 Pro to be precise). This is an arm64 architecture. My goal was to compile for the standard x86 architecture in Lambda.

Yes, arm64 is available as a target, but the availability of x86 is 24 regions (including GovCloud) vs 10 (and no GovCloud). These regions lists are available on the [AWS Lambda pricing page](https://aws.amazon.com/lambda/pricing/).

## Head First

My goals going into this were not very ambitious. That is, beyond jumping in headfirst to running Rust code in Lambda. The code I used was taken straight from the lambda-http crate example. I wanted to get a feel for what the experience was going to be like moving from serverless Python to serverless Rust.

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

#[tokio::main]
async fn main() -> Result<(), Error> {
    let func = handler_fn(func);
    lambda_runtime::run(func).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) }))
}
```

Running `cargo build` here introduced me to Rust's fantastic and helpful compiler errors, and managing my dependencies in `Cargo.toml`.

```
error[E0432]: unresolved import `serde_json`
 --> src/main.rs:2:5
  |
2 | use serde_json::{json, Value};
  |     ^^^^^^^^^^ use of undeclared crate or module `serde_json`

error[E0433]: failed to resolve: use of undeclared crate or module `tokio`
 --> src/main.rs:4:3
  |
4 | #[tokio::main]
  |   ^^^^^ use of undeclared crate or module `tokio`

```

Using `cargo` feels very familiar to me as `pipenv` works much the same way. Nice.

```toml
[package]
name = "rust_demo_api"
version = "0.1.0"
edition = "2021"

[dependencies]
lambda_runtime = "*"
lambda_http = "*"
serde_json = "*"
tokio = "*"
```

Re-running `cargo build` now results in success! Great! Now to compile my function for x86 and deploy is using SAM. The [documentation](https://rust-lang.github.io/rustup/cross-compilation.html) is straightforward and cross compiling is something that is touted with Rust. So I...

```zsh
% rustup target add x86_64-unknown-linux-gnu
% cargo build --target x86_64-unknown-linux-gnu
```

## The Wall

This is where I hit it. The very first thing I tried to do was switch my target from x86 to arm64 to see if I could compile for Graviton.

```zsh
% rustup target add aarch64-unknown-linux-gnu
% cargo build --target aarch64-unknown-linux-gnu
```

More errors.

I begin a process we are all very familiar with: encounter issue, Google appropriate errors and terms, and test solutions. I did indeed find posts and GitHub issues that related to the various compiling errors I was trying to work through, but in the process I came to the realization that I was trying to solve cross compiling issues in the opposite direction of most of the Rust community.

When cross compiling is discussed it is about an x86 host targeting arm64 platforms. I'm trying to go from an arm64 host to an x86 platform. At this point I come back to the Rust runtime's documentation.

> _At the time of writing, the ability to cross compile to x86 or aarch64 AL2 on MacOS is limited. The most robust way we've found is using Docker to produce the artifacts for you._

```zsh
% PLATFORM="linux/amd64" \ # linux/arm64
% TARGET="x86_64-unknown-linux-gnu" # aarch64-unknown-linux-gnu
% docker run \
  --platform "${PLATFORM}"
  --rm --user "$(id -u)":"$(id -g)" \
  -v "${PWD}":/usr/src/myapp -w /usr/src/myapp rust:latest \
  cargo build --release --target "$(TARGET}"
```

The x86 build did not work, but the arm64 build did!

```
Finished release [optimized] target(s) in 2m 56s
```

Oof that's slow. An immediate repeat run clocked in at 58.81 seconds. The update of the `crates.io` index is taking up the bulk of that time, and the compiling will only go so fast as the number of CPUs I assign to Docker.

Still, this is progress. I can copy the resulting binary to `build/get/bootstrap`, the location set in my SAM template shown below, and deploy my first function.

```yaml
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31

Globals:

  Function:
    Architectures:
      - arm64
    Runtime: provided.al2
    Handler: rust

Resources:

  Api:
    Type: AWS::Serverless::HttpApi
    Properties:
      FailOnWarnings: true

  GetFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./build/get
      Events:
        read:
          Type: HttpApi
          Properties:
            ApiId: !Ref Api
            Method: get
            Path: /

Outputs:

  ApiUrl:
    Value: !Sub https://${Api}.execute-api.${AWS::Region}.${AWS::URLSuffix}
```

I had partially achieved my goals. I have a sample Rust function deployed behind an API Gateway that I can invoke. I had to fall back to arm64, and it doesn't use the new SDK to perform operations (i.e. DynamoDB).

## Professional Help

During all of this I had an AWS Solutions Architect guided me through my pitfalls. [Nicolas Moutschen](https://twitter.com/NMoutschen) has a fully realized version of what I was prototyping [sitting in a pull request](https://github.com/aws/aws-sam-cli-app-templates/pull/160/files) to the AWS SAM CLI, and a much larger and more comprehensive Rust application available [here](https://github.com/aws-samples/serverless-rust-demo).

I took the code in his PR (it writes a JSON body to a DynamoDB table) and added a second function.

```yaml
...
  PostFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./build/post
      Policies:
        - DynamoDBWritePolicy:
            TableName: !Ref Table
      Events:
        create:
          Type: HttpApi
          Properties:
            ApiId: !Ref Api
            Method: post
            Path: /create/{id}
...
```

The results were nothing short of fantastic.

{% embed https://twitter.com/bryson3gps/status/1492587703640154115 %}

In addition to learning a great deal from Nicolas' code, I also followed suggestions he made to fix my issue of running builds for x86. He came across [cargo-zigbuild](https://github.com/messense/cargo-zigbuild) which uses [ziglang](https://github.com/ziglang/zig) as the linker for cross compiling.

```zsh
% python3 -m venv zigenv
...
% source zigenv/bin/activate
(zigenv) % pip install ziglang
...
(zigenv) % cargo install cargo-zigbuild
...
(zigenv) % cargo zigbuild --release --target x86_64-unknown-linux-gnu
...
Finished release [optimized] target(s) in 20.85s
```

First try!

That is night and day with the earlier two minutes plus in Docker! The re-run without changes - just as before - clocked at 0.04 seconds. These are the kinds of build speeds I wanted to see!

## This Was the Hard Way

The [Rust Book](https://doc.rust-lang.org/book/) - linked again to emphasize its importance - is a fantastic getting started resource. Kicking the tires on Rust as a language was quick and easy and lived up to much of the promise of having the cake and eating it too.

Jumping immediately into running Rust code in AWS Lambda also _shouldn't_ have been too difficult had I been on an Intel based Mac. I came across what felt like **_ALL_** of the edge cases in cross compiling Rust code.

I still need to learn much of the _how_ of Rust, but seeing working code from Nicolas demonstrating the use of the new SDK in the context of a Lambda function taught me a great deal. If I really wanted to I could probably cobble together a basic CRUDL API with my current knowledge.

Would I use such a thing in production? I'm not nearly that irresponsible.

This adventure did bear unexpected fruits.

{% embed https://twitter.com/dfrasca80/status/1494747974861312002 %}
{% embed https://twitter.com/bryson3gps/status/1494748990809722884 %}
{% embed https://twitter.com/NMoutschen/status/1494751112037031945 %}

I'm going to be continuing my Rust journey by completing the Rust Book. If by the end of this post you haven't gone and followed [@NMoutschen](https://twitter.com/NMoutschen) on Twitter yet you can do so now!

🦀