#実行環境
-
- 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でクロスコンパイルできるようにしておきます。
$ 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で試してみた
#ファイル内容
##.cargo/config
クロスコンパイル用にlinkerを指定します。
[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"
##Cargo.toml
以下はCargo.tomlの内容です。
パッケージ名はhello-rustにしました。
[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 のカスタムランタイム
次に、[dependencies]に lambda_runtime、tokio、serde_json の3つのクレートを追加します。
この3つがRustでLambdaアプリケーションを構築する際の基本クレートです。
今回はその他にエラー処理用に anyhow 、ロガーとして env_logger を採用しています。
##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に渡すハンドラは、以下の制約を満たす必要があります。
-
- 非同期関数であること
-
- 引数にeventとcontextを受け取ること
eventは、serdeがデシリアライズできる型であること
contextはlambda_runtime::Context型であること
戻り値の型はcore::result::Result型かつ、Ok側の型パラメータはserdeがシリアライズできる型であること
この制約を満たせば、例えば以下のようなハンドラにすることも可能です。
async fn lambda_handler(event: Vec<i32>, _: Context) -> Result<String, Error>
###lambda_handler
実際のイベントを処理する関数です。
関数名は任意ですが、ハンドラにするために上述の制約を満たす必要があります。
「1. 非同期関数であること」の制約があるので、 (たとえ内部で非同期処理を行っていなくとも) 関数にasyncキーワードを付ける必要があります。
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を使ってビルドおよびデプロイを行うために、SAMテンプレートを用意します。
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つのタスクを実行する必要があります。
-
- ソースコードのコンパイル
- 出来上がった実行可能ファイルを所定の場所へ移動
それを踏まえて、以下のように記述しました。
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を行います。
sam build
sam deploy --guided
sam deploy時の入力内容は通常通りなので省略しますが、実行すれば以下のようにLambda関数がデプロイされます。
テストを実行してみます。
想定通りの出力が返ってくることが確認できます。
無事、Rustで作ったLambdaアプリケーションをSAMデプロイすることができました。
#おわりに
Rustは実行速度も速いので、AWS Lambdaと相性が良いように感じています。
次はコンテナイメージから構築してSAMデプロイしてみようと思います。
最後までお読み頂きありがとうございました。
質問や不備についてはコメント欄かTwitterまでお願いします。
#参考
カスタムランタイムの構築
Rust Runtime for AWS Lambda