RustでAWS Lambda関数を書いてSAMデプロイする

#実行環境
- macOS Monterey 12.1
- rustup 1.24.3 

#ディレクトリ構造
ディレクトリ構造は下記のようになりました。

```
.
├── .cargo
│   └── config
├── src
│   └── main.rs
├── Cargo.toml
├── Makefile
└── template.yaml
```

これらのファイルを一つずつ見ていきます。
`Makefile`と`template.yaml`は`sam build`時に使用します。


#事前準備
今回はmacOS上でビルドしますが、Lambdaの実行コンテナはLinuxベースのため、クロスコンパイルで対応します。
以下のコマンドを入力し、cargoでクロスコンパイルできるようにしておきます。

```shell
$ brew install filosottile/musl-cross/musl-cross     
$ rustup target add x86_64-unknown-linux-musl
$ ln -s /usr/local/bin/x86_64-linux-musl-gcc /usr/local/bin/musl-gcc
```

参考: [Lambda が Custmon Runtimeに対応したのでRustで試してみた](https://tech.bitbank.cc/lambda-custmon-runtime/)

#ファイル内容
##.cargo/config
クロスコンパイル用にlinkerを指定します。

```:.cargo/config
[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"
```

##Cargo.toml
以下は`Cargo.toml`の内容です。
パッケージ名は`hello-rust`にしました。

```toml:Cargo.toml
[package]
name = "hello-rust"
version = "0.1.0"
edition = "2021"

# binの名前はbootstrapで固定
[[bin]]
name = "bootstrap"
path = "src/main.rs"

[dependencies]
# Lambda用3点セット
lambda_runtime = "0.4"
tokio = "1.15"
serde_json = "1.0"

anyhow = { version = "1.0", features = ["backtrace"] }
log = "0.4"
env_logger = "0.9"
```

まず、生成されるバイナリの名前を`bootstrap`にしておく必要があります。
これはAWS Lambdaでカスタムランタイムを構築した際のエンドポイントとなります。
参考: [AWS Lambda のカスタムランタイム](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/runtimes-custom.html#runtimes-custom-build)

次に、`[dependencies]`に [lambda_runtime](https://crates.io/crates/lambda_runtime)、[tokio](https://crates.io/crates/tokio)、[serde_json](https://crates.io/crates/serde_json) の3つのクレートを追加します。
この3つがRustでLambdaアプリケーションを構築する際の基本クレートです。

今回はその他にエラー処理用に [anyhow](https://crates.io/crates/anyhow) 、ロガーとして [env_logger](https://crates.io/crates/env_logger) を採用しています。

##src/main.rs
次にソースコードを見ていきます。

```rust:src/main.rs
use std::process;

use anyhow::{Context, Result};
use lambda_runtime::handler_fn;
use serde_json::{json, Value};

async fn lambda_handler(event: Value, _: lambda_runtime::Context) -> Result<Value> {
    let name = event["name"].as_str().context("name does not exist.")?;
    Ok(json!({ "message": format!("Hello {}!", name) }))
}

#[tokio::main]
async fn main() {
    env_logger::init();
    let func = handler_fn(lambda_handler);
    if let Err(err) = lambda_runtime::run(func).await {
        log::error!("{:?}", err);
        process::exit(1);
    }
}
```

###main
`main`関数は、Lambda実行コンテナの初回実行時 (いわゆるコールドスタート時) にのみ実行されます。
`lambda_runtime::handler_fn`の引数にLambda関数が呼び出されたときにハンドラとなる関数を渡し、`lambda_runtime::run`でイベントを待ち受けます。

毎回走るような処理ではないので、ここはほとんど定型文になるかと思います。

####ハンドラ関数の制約
`handler_fn`に渡すハンドラは、以下の制約を満たす必要があります。

1. 非同期関数であること
1. 引数にeventとcontextを受け取ること
    1. eventは、`serde`がデシリアライズできる型であること
    1. contextは`lambda_runtime::Context`型であること
1. 戻り値の型は`core::result::Result`型かつ、`Ok`側の型パラメータは`serde`がシリアライズできる型であること

この制約を満たせば、例えば以下のようなハンドラにすることも可能です。

```rust
async fn lambda_handler(event: Vec<i32>, _: Context) -> Result<String, Error>
```

###lambda_handler
実際のイベントを処理する関数です。
関数名は任意ですが、ハンドラにするために上述の制約を満たす必要があります。
「1. 非同期関数であること」の制約があるので、 (たとえ内部で非同期処理を行っていなくとも) 関数に`async`キーワードを付ける必要があります。

```rust
async fn lambda_handler(event: Value, _: lambda_runtime::Context) -> Result<Value> {
    let name = event["name"].as_str().context("name does not exist.")?;
    Ok(json!({ "message": format!("Hello {}!", name) }))
}
```

今回はとりあえずSAMデプロイするまでが目的なので、ハンドラの内容自体は受け取ったイベントから`name`を取り出してJSONを返すのみにとどめました。

##template.yaml
[AWS SAM](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/what-is-sam.html)を使ってビルドおよびデプロイを行うために、SAMテンプレートを用意します。

```yaml:template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Resources:
  HelloRustFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: hello-rust
      Runtime: provided
      # ハンドラ名は使われないが、ないとエラーになるので適当な文字列を入れておく
      Handler: bootstrap.is.real.handler
      CodeUri: .
      Environment:
        Variables:
          RUST_LOG: info
          RUST_BACKTRACE: 1
          RUST_LOG_STYLE: never
    Metadata:
      # makeを使ったビルドを行う
      BuildMethod: makefile
```

`Runtime`には`provided`を指定します。
RustでLambda関数を作成する場合、ビルドした`bootstrap`ファイルがそのまま実行可能ファイルとなります。
そのため`Handler`項目は本来は不要なのですが、SAMの仕様上これがないとデプロイできないので、何か適当な文字列を入れておきます。

それと、`Metadata`属性を宣言して`BuildMethod: makefile`を指定します。
これにより`sam build`時に`make`が走るようになります。
ここでの`makefile`は先頭が小文字なので注意してください。

##Makefile
`sam build`時の`make`用に、プロジェクト直下に`Makefile`を置きます。
ここでは最低でも以下の2つのタスクを実行する必要があります。

1. ソースコードのコンパイル
1. 出来上がった実行可能ファイルを所定の場所へ移動

それを踏まえて、以下のように記述しました。

```:Makefile
build-HelloRustFunction:
	cargo build --release --target x86_64-unknown-linux-musl
	cp ./target/x86_64-unknown-linux-musl/release/bootstrap $(ARTIFACTS_DIR)
```

ターゲット名は`build-【template.yamlに記述した対象リソースの論理ID】`となります。
`Makefile`の仕様上、インデントは (空白ではなく) タブ文字で記述する必要があることに注意してください。

ソースコードをビルドし、`$(ARTIFACTS_DIR)`へ実行バイナリを格納しています。
Windows/macOSの場合、linux-x86_64用にクロスコンパイルしておく必要があります。
この実行バイナリは`sam deploy`でS3へアップロードされる際に自動でzip圧縮されます。

ここでの`$(ARTIFACTS_DIR)`は (デフォルトでは) `.aws-sam/build/HelloRustFunction/`です。

#SAMビルド、SAMデプロイ
これで必要なファイルが出揃ったので、あとはそのまま`sam build`と`sam deploy`を行います。

```shell
sam build
sam deploy --guided
```

`sam deploy`時の入力内容は通常通りなので省略しますが、実行すれば以下のようにLambda関数がデプロイされます。

![hello-rust.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492981/518415e6-05f3-2d56-d4c4-197aa1db56e6.jpeg)

テストを実行してみます。

![test.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492981/5468b895-8de3-2364-c18d-b645871d02eb.jpeg)
![result.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492981/2a633b62-e6d1-1b78-c572-68c4d204ee1c.jpeg)

想定通りの出力が返ってくることが確認できます。
無事、Rustで作ったLambdaアプリケーションをSAMデプロイすることができました。

#おわりに
Rustは実行速度も速いので、AWS Lambdaと相性が良いように感じています。
次はコンテナイメージから構築してSAMデプロイしてみようと思います。

最後までお読み頂きありがとうございました｡
質問や不備についてはコメント欄か[Twitter](https://twitter.com/ka2_kamaboko)までお願いします。

#参考
[カスタムランタイムの構築](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/building-custom-runtimes.html)
[Rust Runtime for AWS Lambda](https://aws.amazon.com/jp/blogs/opensource/rust-runtime-for-aws-lambda/)