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