This package makes it easy to run AWS Lambda Functions written in Rust. This workspace includes multiple crates:
-
lambda-runtime
is a library that provides a Lambda runtime for applications written in Rust. -
lambda-http
is a library that makes it easy to write API Gateway proxy event focused Lambda functions in Rust.
The code below creates a simple function that receives an event with a firstName
field and returns a message to the caller. Notice: this crate is tested against latest stable 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) }))
}
There are currently multiple ways of building this package: manually with the AWS CLI, and with the Serverless framework.
To deploy the basic sample as a Lambda function using the AWS CLI, we first need to manually build it with cargo
. Due to a few differences in dependencies, the process for building for Amazon Linux 2 is slightly different than building for Amazon Linux.
Building for Amazon Linux 2
Decide which target you'd like to use. For ARM Lambda Functions you'll want to use aarch64-unknown-linux-gnu
and for x86 Lambda Functions you'll want to use x86_64-unknown-linux-gnu
.
Run this script once to add your desired target, in this example we'll use x86:
$ rustup target add x86_64-unknown-linux-gnu
Compile one of the examples as a release with a specific target for deployment to AWS:
$ cargo build -p lambda_runtime --example basic --release --target x86_64-unknown-linux-gnu
Building on MacOS Using Docker
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. This guide will work for both Intel and Apple Silicon MacOS and requires that you have set up Docker correctly for either architecture. See here for a guide on how to do this.
The following command will pull the official Rust Docker Image for a given architecture you plan to use in Lambda and use it to run any cargo commands you need, such as build.
$ LAMBDA_ARCH="linux/arm64" # set this to either linux/arm64 for ARM functions, or linux/amd64 for x86 functions.
$ RUST_TARGET="aarch64-unknown-linux-gnu" # corresponding with the above, set this to aarch64 or x86_64 -unknown-linux-gnu for ARM or x86 functions.
$ RUST_VERSION="latest" # Set this to a specific version of rust you want to compile for, or to latest if you want the latest stable version.
$ docker run \
--platform ${LAMBDA_ARCH} \
--rm --user "$(id -u)":"$(id -g)" \
-v "${PWD}":/usr/src/myapp -w /usr/src/myapp rust:${RUST_VERSION} \
cargo build -p lambda_runtime --example basic --release --target ${RUST_TARGET} # This line can be any cargo command
In short, the above command does the following:
- Gives the current user ownership of the artifacts produced by the cargo run.
- Mounts the current working directory as a volume within the pulled Docker image.
- Pulls a given Rust Docker image for a given platform.
- Executes the command on the line beginning with
cargo
within the project directory within the image.
It is important to note that build artifacts produced from the above command can be found under the expected target/
directory in the project after build.
Building for Amazon Linux 1
Run this script once to add the new target:
$ rustup target add x86_64-unknown-linux-musl
- Note: If you are running on Mac OS you'll need to install the linker for the target platform. You do this using the
musl-cross
tap from Homebrew which provides a complete cross-compilation toolchain for Mac OS. Oncemusl-cross
is installed we will also need to inform cargo of the newly installed linker when building for thex86_64-unknown-linux-musl
platform.
$ brew install filosottile/musl-cross/musl-cross
$ mkdir .cargo
$ echo $'[target.x86_64-unknown-linux-musl]\nlinker = "x86_64-linux-musl-gcc"' > .cargo/config
Compile one of the examples as a release with a specific target for deployment to AWS:
$ cargo build -p lambda_runtime --example basic --release --target x86_64-unknown-linux-musl
Uploading the Resulting Binary to Lambda
For a custom runtime, AWS Lambda looks for an executable called bootstrap
in the deployment package zip. Rename the generated basic
executable to bootstrap
and add it to a zip archive.
NOTE: Depending on the target you used above, you'll find the provided basic executable under the corresponding directory. In the following example, we've compiled for x86_64-unknown-linux-gnu.
$ cp ./target/x86_64-unknown-linux-gnu/release/examples/basic ./bootstrap && zip lambda.zip bootstrap && rm bootstrap
Now that we have a deployment package (lambda.zip
), we can use the AWS CLI to create a new Lambda function. Make sure to replace the execution role with an existing role in your account!
$ aws lambda create-function --function-name rustTest \
--handler doesnt.matter \
--zip-file fileb://./lambda.zip \
--runtime provided.al2 \ # Change this to provided.al if you would like to use Amazon Linux 1.
--role arn:aws:iam::XXXXXXXXXXXXX:role/your_lambda_execution_role \
--environment Variables={RUST_BACKTRACE=1} \
--tracing-config Mode=Active
You can now test the function using the AWS CLI or the AWS Lambda console
$ aws lambda invoke --function-name rustTest \
--payload '{"command": "Say Hi!"}' \
output.json
$ cat output.json # Prints: {"msg": "Command Say Hi! executed."}
Note: --cli-binary-format raw-in-base64-out
is a required
argument when using the AWS CLI version 2. More Information
Alternatively, you can build a Rust-based Lambda function declaratively using the Serverless framework Rust plugin.
A number of getting started Serverless application templates exist to get you up and running quickly
- a minimal echo function to demonstrate what the smallest Rust function setup looks like
- a minimal http function to demonstrate how to interface with API Gateway using Rust's native http crate (note this will be a git dependency until 0.2 is published)
- a combination multi function service to demonstrate how to set up a services with multiple independent functions
Assuming your host machine has a relatively recent version of node, you won't need to install any host-wide serverless dependencies. To get started, run the following commands to create a new lambda Rust application and install project level dependencies.
$ npx serverless install \
--url https://github.com/softprops/serverless-aws-rust \
--name my-new-app \
&& cd my-new-app \
&& npm install --silent
Deploy it using the standard serverless workflow
# build, package, and deploy service to aws lambda
$ npx serverless deploy
Invoke it using serverless framework or a configured AWS integrated trigger source:
$ npx serverless invoke -f hello -d '{"foo":"bar"}'
Alternatively, you can build a Rust-based Lambda function in a docker mirror of the AWS Lambda provided runtime with the Rust toolchain preinstalled.
Running the following command will start a ephemeral docker container which will build your Rust application and produce a zip file containing its binary auto-renamed to bootstrap
to meet the AWS Lambda's expectations for binaries under target/lambda_runtime/release/{your-binary-name}.zip
, typically this is just the name of your crate if you are using the cargo default binary (i.e. main.rs
)
# build and package deploy-ready artifact
$ docker run --rm \
-v ${PWD}:/code \
-v ${HOME}/.cargo/registry:/root/.cargo/registry \
-v ${HOME}/.cargo/git:/root/.cargo/git \
softprops/lambda-rust
With your application build and packaged, it's ready to ship to production. You can also invoke it locally to verify is behavior using the lambdaci :provided docker container which is also a mirror of the AWS Lambda provided runtime with build dependencies omitted.
# start a docker container replicating the "provided" lambda runtime
# awaiting an event to be provided via stdin
$ unzip -o \
target/lambda/release/{your-binary-name}.zip \
-d /tmp/lambda && \
docker run \
-i -e DOCKER_LAMBDA_USE_STDIN=1 \
--rm \
-v /tmp/lambda:/var/task \
lambci/lambda:provided
# provide an event payload via stdin (typically a json blob)
# Ctrl-D to yield control back to your function
Lambdas can be run and debugged locally using a special Lambda debug proxy (a non-AWS repo maintained by @rimutaka), which is a Lambda function that forwards incoming requests to one AWS SQS queue and reads responses from another queue. A local proxy running on your development computer reads the queue, calls your lambda locally and sends back the response. This approach allows debugging of Lambda functions locally while being part of your AWS workflow. The lambda handler code does not need to be modified between the local and AWS versions.
lambda_runtime
is a library for authoring reliable and performant Rust-based AWS Lambda functions. At a high level, it provides a few major components:
Handler
, a trait that defines interactions between customer-authored code and this library.lambda_runtime::run
, function that runs anHandler
.
The function handler_fn
converts a rust function or closure to Handler
, which can then be run by lambda_runtime::run
.
This project does not currently include Lambda event struct definitions though we intend to do so in the future. Instead, the community-maintained aws_lambda_events
crate can be leveraged to provide strongly-typed Lambda event structs. You can create your own custom event objects and their corresponding structs as well.
To serialize and deserialize events and responses, we suggest using the use the serde
library. To receive custom events, annotate your structure with Serde's macros:
use serde::{Serialize, Deserialize};
use serde_json::json;
use std::error::Error;
#[derive(Serialize, Deserialize)]
pub struct NewIceCreamEvent {
pub flavors: Vec<String>,
}
#[derive(Serialize, Deserialize)]
pub struct NewIceCreamResponse {
pub flavors_added_count: usize,
}
fn main() -> Result<(), Box<Error>> {
let flavors = json!({
"flavors": [
"Nocciola",
"抹茶",
"आम"
]
});
let event: NewIceCreamEvent = serde_json::from_value(flavors)?;
let response = NewIceCreamResponse {
flavors_added_count: event.flavors.len(),
};
serde_json::to_string(&response)?;
Ok(())
}