Rust で書いた AWS Lambda を動かす
はじめに
Rust を触っていて Lambda で Rust が動くなら動かしてみたいなと思いました。普段 Lambda のコードを書く時は以前書いたように AWS SAM CLI を使っているので今回もまずは SAM を使ってテンプレートを作り、せっかくなので AWS CDK でもデプロイする手順をまとめました。リポジトリはこちらです。
Rust で書いた Lambda を動かすには
コンテナイメージを使う場合
Lambda にコードをデプロイする方法は二種類(コンテナイメージを使う方法と zip ファイルを使う方法)あり今回はコンテナイメージを使うことにします。ドキュメントに Lambda 向けに AWS が提供しているベースイメージがまとまっており例えば Python なら ECR Public だとここに、Node.js ならここに公開されています。残念ながら Rust のベースイメージは 2022/03/30 時点だと公開されていないのでカスタムランタイム用のベースイメージを使います。
カスタムランタイム
ここで出てきたカスタムランタイムについて少し補足します。まずカスタムランタイムのドキュメントはこちらです。Lambda のランタイムはハンドラー関数の実行をおこなったり、レスポンスを Lambda に返すことを行ったりするもので、任意の言語で実装することができます。幸いなことに Lambda 用の Rust ランタイムがこちらで公開されているのでこれを使って実装していけば比較的簡単に Rust で書いた Lambda を実行することができます。Rust で実装したコードをコンパイルし、Amazon Linux 2 のベースイメージのあるディレクトリ配下に配置すればよいです。
Dockerfile をみてみる
公開している Dockerfile を見てみましょう。
FROM rust:1.59.0 as build-image
WORKDIR /rust/hello_world
COPY src/ /rust/hello_world/src/
COPY Cargo.toml /rust/hello_world/
RUN rustup update && \
rustup target add x86_64-unknown-linux-musl
RUN cargo build --release --target x86_64-unknown-linux-musl
FROM public.ecr.aws/lambda/provided:al2
COPY /rust/hello_world/target/x86_64-unknown-linux-musl/release/bootstrap ${LAMBDA_RUNTIME_DIR}/bootstrap
CMD [ "lambda-handler" ]
マルチステージビルドを使って Rust のイメージ内でコンパイルを行い、 Amazon Linux 2 のベースイメージにコピーしています。カスタムランタイムだと実行ファイルを bootstrap という名前にする必要がるのでそうしています。
ハンドラーのテスト
今回実装したハンドラーは event から message を取り出してそれに Hello という文字列をつけて返すだけの簡単な物です。公開しているハンドラーの特にテストコードを見てみましょう。
#[cfg(test)]
mod tests {
use super::*;
use http::{HeaderMap, HeaderValue};
use lambda_runtime::Context;
#[tokio::test]
async fn handler_with_msg() -> Result<(), Error> {
let mut headers = HeaderMap::new();
headers.insert(
"lambda-runtime-aws-request-id",
HeaderValue::from_static("my-id"),
);
headers.insert(
"lambda-runtime-deadline-ms",
HeaderValue::from_static("123"),
);
let context = Context::try_from(headers).unwrap();
let event = json!({
"message": "AWS Lambda on Rust"
});
let expected = json!({
"message": "Hello, AWS Lambda on Rust!",
});
let response = handler(LambdaEvent::new(event, context)).await?;
assert_eq!(response, expected);
Ok(())
}
}
Python でハンドラー関数を書いているとハンドラーの引数は event , context の 2 つですが、Rust のランタイムだと引数は event: LambdaEvent<Value> の 1 つだけのようです。 event.into_parts() で event , context を取り出すわけです。テストコードで LambdaEvent を生成するにはドキュメントをみる感じだと LambdaEvent::new(event, context) という風に event , context を渡せばよさそうでこの context は Context::try_from で生成できます。 Context のドキュメントをみると何やらメンバーがたくさんあって、テストを書くにもこれを全部設定しないといけないのかなぁって最初思ってました。実はそんなことはなくて Context::try_from のコード を一部抜粋するとこんな感じです。
let ctx = Context {
request_id: headers
.get("lambda-runtime-aws-request-id")
.expect("missing lambda-runtime-aws-request-id header")
.to_str()?
.to_owned(),
deadline: headers
.get("lambda-runtime-deadline-ms")
.expect("missing lambda-runtime-deadline-ms header")
.to_str()?
.parse::<u64>()?,
invoked_function_arn: headers
.get("lambda-runtime-invoked-function-arn")
.unwrap_or(&HeaderValue::from_static(
"No header lambda-runtime-invoked-function-arn found.",
))
.to_str()?
.to_owned(),
xray_trace_id: headers
.get("lambda-runtime-trace-id")
.unwrap_or(&HeaderValue::from_static(""))
.to_str()?
.to_owned(),
client_context,
identity,
..Default::default()
};
このうち request_id , deadline はどちらも headers.get().expect() で生成されています。 expect メソッドが使われているので headers.get() で None が返った場合はパニックします。一方で invoked_function_arn と xray_trace_id は headers.get().unwrap_or() で生成されています。こちらは unwrap_or メソッドが使われているので headers.get() で None が返ってもデフォルト値が生成されてパニックしません。というわけで request_id と deadline だけをテストコードの中で context に含める必要があり、ハンドラーのテストコードもそのように実装しています。
デプロイと Lambda の実行
README に SAM と CDK でのデプロイ方法と実行方法をまとめているのでよかったら試してください。
おわりに
これで簡単なコードなら Rust で書いても Lambda で動かせるようになりました。AWS SDK for Rust と組み合わせて例えば DynamoDB に読み書きするようなコードも書いてみたいです。
Discussion