Rust: using Lambda arm64 architecture

Rust: using Lambda arm64 architecture

This blog post explains how to leverage Graviton2 with your Lambda function.

As a part of the series Serverless Rust you can check out the other parts:

Part 1 describes how to set up Rust and VsCode.

Graviton2

Apparently, AWS Graviton claim Graviton2 processors provide better price performance over comparable current-generation x86-based instances. Furthermore, functions that use arm64 architecture (Graviton2 processor) offer a lower cost per Gb/s compared with the equivalent function running on an x86-based CPU and from the Lambda Pricing page it seems to provide up to 34% price-performance improvement. The 20% reduction in duration costs also applies when using Provisioned Concurrency. You can further reduce your costs by up to 17% with Compute Savings Plans.

Project structure

The structure of the project is pretty standard.

Screenshot 2022-01-06 at 17.57.50.png

In this post, we focus on some snippets of a few files.

  • cargo.toml
  • Makefile
  • template.yml

cargo.toml

Each Rust project has this file cargo.toml. Cargo is Rust's build system and package manager.

You can add library

cargo add lambda_runtime

You can build your project.

cargo build

[package]
name = "my-app"
version = "0.1.0"
edition = "2022"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
futures = "0.3.17"
aws-config = "0.3.0"
aws-types = "0.3.0"
log = "0.4.14"
serde = {version = "1.0", features = ["derive"] }
serde_json = "1.0.68"
simple_logger = "1.13.0"
tokio = "1.13.0"
lambda_runtime = "0.4.1"
aws_lambda_events = "0.5.0"

# APP
[[bin]]
name = "handler"
path = "src/bin/lambda/handler.rs"

A helpful VsCode extension is crates because it will help you with the versions

Screenshot 2022-01-06 at 18.14.16.png

The last note on this file is the bin part. The default binary filename is src/main.rs, defaulting to the package's name. Additional binaries are stored in the src/bin/ directory, and you could have, for example:

# APP
[[bin]]
name = "app-random-api"
path = "src/bin/lambda/app-random-api.rs"

[[bin]]
name = "app-lambda-authorizer"
path = "src/bin/lambda/app-lambda-authorizer.rs"

Makefile

Lambda function run Rust as custom runtime, and so we need to declare the Metadata resource attribute with a BuildMethod: makefile entry. The Makefile is responsible for compiling the custom runtime and copying the build artefacts. The location of the Makefile is specified by the CodeUri property of the function resource and must be named Makefile.

# the same name that you find in the cargo.toml bin section
FUNCTIONS := app-random-api app-lambda-authorizer

ARCH := aarch64-unknown-linux-gnu

build:
  rm -rf ./build
  rm -rf ./target
  cross build --release --target $(ARCH)
  mkdir -p ./build
  ${MAKE} ${MAKEOPTS} $(foreach function,${FUNCTIONS}, build-${function})

build-%:
  mkdir -p ./build/$*
  cp -v ./target/$(ARCH)/release/$* ./build/$*/bootstrap

deploy:
  sam deploy --guided --no-fail-on-empty-changeset --no-confirm-changeset --stack-name xxx --template-file template.yml 

delete:
  sam delete --stack-name xxxx

To be able to use arm64 architecture

  • we could install CROSS based on Docker and it will have some issue with M1 Macs
  • we could install cargo-zigbuild and Zig for cross-compilation

if you don't and run

cargo build --release --target x86_64-unknown-linux-musl

You will get this beautiful error:

error: failed to run custom build command for `ring v0.16.20`

Caused by:
 process didn't exit successfully: `/Users/fra0005d/git/rust-examples/rust-dynamodb/target/release/build/ring-e4e3d5f387d41c64/build-script-build` (exit status: 101)
 --- stdout
 OPT_LEVEL = Some("3")
 TARGET = Some("x86_64-unknown-linux-musl")
 HOST = Some("x86_64-apple-darwin")
 CC_x86_64-unknown-linux-musl = None
 CC_x86_64_unknown_linux_musl = None
 TARGET_CC = None
 CC = None
 CROSS_COMPILE = None
 CFLAGS_x86_64-unknown-linux-musl = None
 CFLAGS_x86_64_unknown_linux_musl = None
 TARGET_CFLAGS = None
 CFLAGS = None
 CRATE_CC_NO_DEFAULTS = None
 DEBUG = Some("false")
 CARGO_CFG_TARGET_FEATURE = Some("fxsr,sse,sse2")

 --- stderr
 running "musl-gcc" "-O3" "-ffunction-sections" "-fdata-sections" "-fPIC" "-m64" "-I" "include" "-Wall" "-Wextra" "-pedantic" "-pedantic-errors" "-Wall" "-Wextra" "-Wcast-align" "-Wcast-qual" "-Wconversion" "-Wenum-compare" "-Wfloat-equal" "-Wformat=2" "-Winline" "-Winvalid-pch" "-Wmissing-field-initializers" "-Wmissing-include-dirs" "-Wredundant-decls" "-Wshadow" "-Wsign-compare" "-Wsign-conversion" "-Wundef" "-Wuninitialized" "-Wwrite-strings" "-fno-strict-aliasing" "-fvisibility=hidden" "-fstack-protector" "-g3" "-U_FORTIFY_SOURCE" "-DNDEBUG" "-c" "-o/Users/fra0005d/git/rust-examples/rust-dynamodb/target/x86_64-unknown-linux-musl/release/build/ring-e7f51b4231eb5b63/out/aesni-x86_64-elf.o" "/Users/fra0005d/.cargo/registry/src/github.com-1ecc6299db9ec823/ring-0.16.20/pregenerated/aesni-x86_64-elf.S"
 thread 'main' panicked at 'failed to execute ["musl-gcc" "-O3" "-ffunction-sections" "-fdata-sections" "-fPIC" "-m64" "-I" "include" "-Wall" "-Wextra" "-pedantic" "-pedantic-errors" "-Wall" "-Wextra" "-Wcast-align" "-Wcast-qual" "-Wconversion" "-Wenum-compare" "-Wfloat-equal" "-Wformat=2" "-Winline" "-Winvalid-pch" "-Wmissing-field-initializers" "-Wmissing-include-dirs" "-Wredundant-decls" "-Wshadow" "-Wsign-compare" "-Wsign-conversion" "-Wundef" "-Wuninitialized" "-Wwrite-strings" "-fno-strict-aliasing" "-fvisibility=hidden" "-fstack-protector" "-g3" "-U_FORTIFY_SOURCE" "-DNDEBUG" "-c" "-o/Users/fra0005d/git/rust-examples/rust-dynamodb/target/x86_64-unknown-linux-musl/release/build/ring-e7f51b4231eb5b63/out/aesni-x86_64-elf.o" "/Users/fra0005d/.cargo/registry/src/github.com-1ecc6299db9ec823/ring-0.16.20/pregenerated/aesni-x86_64-elf.S"]: No such file or directory (os error 2)', /Users/fra0005d/.cargo/registry/src/github.com-1ecc6299db9ec823/ring-0.16.20/build.rs:653:9
 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
warning: build failed, waiting for other jobs to finish...
error: build failed

AWS SAM

The last and more straightforward step is the serverless template with AWS SAM.

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: sam-app

Globals:
  Function:
    MemorySize: 1024
    Architectures: ["arm64"]
    Handler: bootstrap
    Runtime: provided.al2
    Timeout: 29
    Environment:
      Variables:
        RUST_BACKTRACE: 1
        RUST_LOG: info

Resources:
  LambdaRandomFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: build/app-random-api/
      Policies:
        - AWSLambdaBasicExecutionRole
    Metadata:
      BuildMethod: makefile
  LambdaAuthorizerFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: build/app-lambda-authorizer/
      Policies:
        - AWSLambdaBasicExecutionRole
    Metadata:
      BuildMethod: makefile

Outputs:
  LambdaRandomFunction:
    Description: "Hello Rust Lambda Function ARN"
    Value: !GetAtt LambdaRandomFunction.Arn
  LambdaAuthorizerFunction:
    Description: "Hello Rust Lambda Function ARN"
    Value: !GetAtt LambdaAuthorizerFunction.Arn

Conclusion

It can be frustrating hitting this problem without help. However, the Rust community is constructive, and in the case of using Rust on AWS, many Solution Architects are happy to help you. So they did with me, and I hope this post will help you speed up your journey with Rust.