🦀

Rust で書いた AWS Lambda を動かす

2022/04/03に公開

はじめに

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  --from=build-image /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 を渡せばよさそうでこの contextContext::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_arnxray_trace_idheaders.get().unwrap_or() で生成されています。こちらは unwrap_or メソッドが使われているので headers.get() で None が返ってもデフォルト値が生成されてパニックしません。というわけで request_iddeadline だけをテストコードの中で context に含める必要があり、ハンドラーのテストコードもそのように実装しています。

デプロイと Lambda の実行

README に SAM と CDK でのデプロイ方法と実行方法をまとめているのでよかったら試してください。

おわりに

これで簡単なコードなら Rust で書いても Lambda で動かせるようになりました。AWS SDK for Rust と組み合わせて例えば DynamoDB に読み書きするようなコードも書いてみたいです。

Discussion