はじめに
- 基本的にAWSのラムダはRuby2.7ランタイムでServerlessFrameworkを用いてデプロイしている。
- Rust言語でラムダ関数をデプロイできると聞いていたものの試したことはなく、思い出したのでせっかうだから試してみたら、意外と躓いてしまった。
- 結果的に大した話ではなかったのだが、同じ沼にはまってしまっている人がいないとも限らないので残しておく。
実行環境
- Ubuntu20.04 on WSL2
- rustup 1.24.3 (ce5817a94 2021-05-31)
- cargo 1.56.0 (4ed5d137b 2021-10-04)
- serverless@3.4.0
- serverless-rust@0.3.8
普通にラムダ関数を作る
コードの用意
AWSがRust向けのSDKを提供してくれているので、いつもお世話になっているrusotoではなく、lambda_runtimeというクレートを使用する。
[package]
name = "lambda-rust"
version = "0.1.0"
edition = "2021"
[dependencies]
lambda_runtime = "0.5"
serde_json = "1.0.79"
tokio = { version = "1", features = ["full"] }
use lambda_runtime::{
Error as LambdaError,
LambdaEvent,
};
async fn handler(event: LambdaEvent<serde_json::Value>) -> Result<serde_json::Value, LambdaError> {
println!("{:?}", event);
Ok(event.payload)
}
#[tokio::main]
async fn main() -> Result<(), LambdaError> {
let handler_fn = lambda_runtime::service_fn(handler);
lambda_runtime::run(handler_fn).await?;
Ok(())
}
ビルド
クロスコンパイルができるように、事前にターゲットを追加しておく必要がある。
$ rustup target add x86_64-unknown-linux-musl
$ cargo build --release--target x86_64-unknown-linux-musl
ビルドを行うと target/x86_64-unknown-linux-musl/release
というディレクトリができており、その中にバイナリが作成されている。
バイナリを bootstrap
にリネームし、 lambda.zip
という名前でZIPファイルを作成。
$ cp ./target/x86_64-unknown-linux-musl/release/{app_name} ./bootstrap
$ zip lambda.zip bootstrap
AWS CLIを使用して関数を作成する。
$ aws lambda create-function \
--profile your_profile_name
--function-name lambda-rust \
--runtime provided.al2 \
--zip-file file://./lambda.zip \
--handler bootstrap \
--role arn:aws:iam::000000000000:role/xxxxx
ServerlessFrameworkを利用する
serverless-rust
というプラグインが用意されているのでこれを利用する。
cargo
コマンドを使用してRustプロジェクトを作成。
serverless.ymlはプロジェクト直下に配置する。以下のような構成になっていればOK。
myapp/
├── .gitignore
├── Cargo.toml
├── serverless.yml
└── src/
└── main.rs
次にserverless.ymlを設定します。
試行錯誤① Rustのバージョン
serverless-rustのリファレンスを読み、使用できるdockerイメージはほかの方の記事を参考にし、とりあえず次のように設定。
service: lambda-rust
frameworkVersion: '3'
provider:
name: aws
runtime: rust
region: ap-northeast-1
custom:
rust:
# custom docker tag
dockerTag: '1.51'
# custom docker image
dockerImage: 'softprops/lambda-rust'
functions:
lambda-rust: # Rustパッケージ名
handler: lambda-rust # こちらも
package:
individually: true
plugins:
- serverless-rust
プラグインをインストールしておく。
$ npm i -D serverless-rust
# もしくは
$ serverless plugin install -n serverless-rust
しかしこの状態で実行すると次のようなエラーが発生する。
error[E0658]: `while` is not allowed in a `const fn`
--> /root/.cargo/registry/src/github.com-1ecc6299db9ec823/http-0.2.6/src/header/value.rs:86:9
|
86 | / while i < bytes.len() {
87 | | if !is_visible_ascii(bytes[i]) {
88 | | ([] as [u8; 0])[0]; // Invalid header value
89 | | }
90 | | i += 1;
91 | | }
| |_________^
|
= note: see issue #52000 <https://github.com/rust-lang/rust/issues/52000> for more information
...(ほかにもいくつかの構文エラー)
error: build failed
Rust build encountered an error: undefined 1.
どうやら、dockerで使用しているRustのバージョンが古いらしい。他のdockerイメージを使用したところ、そもそもCargo.tomlのエディションに2021を指定するとエラーになるものもあったりと、上手くいかない。
試行錯誤② ターゲットが指定できない
使用している lambda_runtime
クレートのバージョンを下げて実行してみる。
use lambda_runtime::{
self as lambda,
Context,
Error as LambdaError,
};
async fn handler(
event: serde_json::Value,
_context: Context,
) -> Result<serde_json::Value, LambdaError> {
println!("{:?}", event);
Ok(event)
}
#[tokio::main]
async fn main() -> Result<(), LambdaError> {
let handler_fn = lambda::handler_fn(handler);
lambda::run(handler_fn).await?;
Ok(())
}
デプロイは完了するものの、コンソールで関数のテストをしてみると次のようなエラーが発生した。
/var/task/bootstrap: /lib64/libc.so.6: version `GLIBC_2.18' not found (required by /var/task/bootstrap)
調べたところ、ターゲットがMUSLになっていないらしい。
プラグインのリファレンスを見ると、ビルド時にフラグを指定するために cargoFlags
が利用できるらしい。
次のように serverless.ymlを修正した。
custom:
rust:
# custom docker tag
dockerTag: '1.51'
# custom docker image
dockerImage: 'softprops/lambda-rust'
# flags passed to cargo
cargoFlags: '--target x86_64-unknown-linux-musl'
ちなみに --release
はデフォルトで指定してくれるので、ここに書いてしまうと2回指定されているとエラーになってしまう。
実行するとまたしてもエラーになった。
error[E0463]: can't find crate for `core`
|
= note: the `x86_64-unknown-linux-musl` target may not be installed
もちろんターゲットをインストールしていないと、フラグを渡してもエラーになってしまう。このdockerイメージでは、MUSLターゲットのインストールは記述されていなかった。
ちなみにmuslを
解決法① Dockerイメージを自作する
ではその足りないターゲットをインストールするために、Dockerfileを作成しコマンドを追加してやり、それをビルドして作成された自前のイメージを使用することで解決できる。
もちろん一から組み立てるようにDockerfileを記述してもよい。
しかし極力作業量を減らすために別のアプローチを試すことにした。
試行錯誤③ ローカルビルド
プラグインのリファレンスには、Dockerを使用しない方法も提示されていた。
次のように serverless.yml を修正した。
service: lambda-rust
frameworkVersion: '3'
provider:
name: aws
runtime: rust
region: ap-northeast-1
custom:
rust:
dockerless: true # <===
target: 'x86_64-unknown-linux-musl'
functions:
lambda-rust:
handler: lambda-rust
package:
individually: true
plugins:
- serverless-rust
cargoFlag
ではなく、 target
というキーでMUSLを指定する。
これにより自身の環境でターゲットがインストールされていれば、デプロイを完了させることができる。
またエディションも2021が利用できるので、 lambda_runtime
クレートも最新版を使用することができる。
Dockerイメージを使用しないことの欠点として、ビルド環境を統一できないため場合によっては動かなくなってしまう可能性もはらんでしまっている。
(Rubyのビルドと違い、Rustのクロスコンパイルはどの環境でビルドしてもクロスcompile先で動くようになるのだろうか?)
しかしながら、最初に行ったビルド → バイナリのリネーム → ZIPファイルの作成、といった作業が不要になるのはすごく便利なので、Dockerfileを用意して最初の一回だけイメージの作成を行い、常にそれを使用するように設定するだけの価値はありそう。
解決策② そもそもカスタム設定が要らなかった
実は、serverless.ymlでカスタムの設定を何も指定しなければ、これまで試行錯誤した中で発生した問題はすべて解決できる。
service: lambda-rust
frameworkVersion: '3'
provider:
name: aws
runtime: rust
region: ap-northeast-1
functions:
lambda-rust:
handler: lambda-rust
package:
individually: true
plugins:
- serverless-rust
これだけでよいです。デフォルト設定が使用され、デプロイやテストは全て問題なく動きます。
何らかのDockerイメージは使用されているので、場合によってはバージョンが追いついていないということもあるかもしれませんが、現時点ではこれが一番安定しているようです。
結論
リファレンスはちゃんと読もう