背景
個人的に家計管理のために Slack に投稿すると IFTTT 経由でスプレッドシートに転記するような AWS Lambda を作って使っていた。
Go 1.x が Deprecation になるのに合わせて Go で書いていたアプリケーションを Rust に移植してみた。 そのときの手順やハマったポイントについて書き残しておく。
ちなみに Python 版の話は以前ブログで書いた。
方法
Rust で書かれた AWS Lambda 関数を便利にビルド・デプロイできるパッケージが AWS 公式でリリースされている。 基本的に手順は README を読みながら進めれば問題なくビルド・デプロイできる。
Cargo Lambda 自体のドキュメントは以下にまとめられている。
手順
1. Cargo Lambda をインストールする
brew tap cargo-lambda/cargo-lambda brew install cargo-lambda
2. Lambda 関数を実装する
Cargo Lambda のサブコマンド new
を利用すると雛形が作られるので、それをもとに修正を行う。
cargo lambda new $LAMBDA_PACKAGE_NAME
以下のインタラクティブを経て生成される。
? Is this function an HTTP function? (y/N) [type `yes` if the Lambda function is triggered by an API Gateway, Amazon Load Balancer(ALB), or a Lambda URL] ? AWS Event type that this function receives activemq::ActiveMqEvent autoscaling::AutoScalingEvent chime_bot::ChimeBotEvent cloudwatch_events::CloudWatchEvent cloudwatch_logs::CloudwatchLogsEvent cloudwatch_logs::CloudwatchLogsLogEvent v codebuild::CodeBuildEvent [↑↓ to move, tab to auto-complete, enter to submit. Leave it blank if you don't want to use any event from the aws_lambda_events crate]
1つ目の質問を No、2つ目の質問を空白で答えると以下のようなコードが生成される
use lambda_runtime::{run, service_fn, Error, LambdaEvent}; use serde::{Deserialize, Serialize}; /// This is a made-up example. Requests come into the runtime as unicode /// strings in json format, which can map to any structure that implements `serde::Deserialize` /// The runtime pays no attention to the contents of the request payload. #[derive(Deserialize)] struct Request { command: String, } /// This is a made-up example of what a response structure may look like. /// There is no restriction on what it can be. The runtime requires responses /// to be serialized into json. The runtime pays no attention /// to the contents of the response payload. #[derive(Serialize)] struct Response { req_id: String, msg: String, } /// This is the main body for the function. /// Write your code inside it. /// There are some code example in the following URLs: /// - https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/examples /// - https://github.com/aws-samples/serverless-rust-demo/ async fn function_handler(event: LambdaEvent<Request>) -> Result<Response, Error> { // Extract some useful info from the request let command = event.payload.command; // Prepare the response let resp = Response { req_id: event.context.request_id, msg: format!("Command {}.", command), }; // Return `Response` (it will be serialized to JSON automatically by the runtime) Ok(resp) } #[tokio::main] async fn main() -> Result<(), Error> { tracing_subscriber::fmt() .with_max_level(tracing::Level::INFO) // disable printing the name of the module in every log line. .with_target(false) // disabling time is handy because CloudWatch will add the ingestion time. .without_time() .init(); run(service_fn(function_handler)).await }
生成された function_handler
関数が実行のメインなのでここに実装を行う。
別パッケージで処理を行うハンドラを書いておいて、それを呼び出すだけにした方が実装が簡潔になると思う。
注意点としては、 aws lambda invoke
で --payload の key になる値に Request 構造体のフィールド名を一致させる必要がある。
3. Lambda 関数をビルドする
Cargo Lambda のサブコマンド build
でビルドできる。
--release
オプションでリリースモードでビルドされるところや、--arm64
オプションで arm64 アーキでビルドされる(--target aarch64-unknown-linux-gnu
の省略形)ところは通常の cargo と同じ。
ちなみに --output-format
でバイナリファイルが Zip された状態で生成することもできる。後述の cargo deploy
を使わずにデプロイする場合には指定すると良いかもしれない。
cargo lambda build --release --arm64 --bin $LAMBDA_PACKAGE_NAME
4. Lambda 関数をデプロイする
cargo lambda のサブコマンド deploy
でデプロイできる。
環境変数も AWS CLI のように一つ一つ指定しなくても --env-file
オプションで KEY=VALUE フォーマットで記述された .env ファイルのパスを渡すだけで設定できる。
Lambda の実行ロールは指定しなければ、デフォルトのロールが作成される。また細かい設定(ハンドラ名、Lambda パッケージのパス、アーキテクチャ等)の指定も不要。
cargo lambda deploy $LAMBDA_FUNCTION_NAME --env-file .env
5. Lambda 関数を実行する(テストする)
実行も cargo lambda のサブコマンド invoke
で実行できる。
--remote
オプションですでに AWS Lambda にデプロイされた関数を実行できる。指定しない場合は(デフォルトでは)ローカルのエミュレータに対してリクエストを送ってしまう*1ので注意する必要がある。
また payload が不要な場合でも version 1.0.0 時点では何らかのデータフラグ(--data-file
, --data-ascii
, --data-example
のいずれか)の指定が必要になっている。
cargo lambda invoke $LAMBDA_FUNCTION_NAME --remote --output-format json --data-ascii "{}"
ハマったポイント
いくつか実装中にハマったポイントがあったので列挙する。
1. OpenSSL 関連のエラー
OpenSSL 関連のエラーメッセージ例
error: failed to run custom build command for `openssl-sys v0.9.97` Caused by: process didn't exit successfully: `$PWD/target/release/build/openssl-sys-b2444d8dd69cea00/build-script-main` (exit status: 101) --- stdout cargo:rerun-if-env-changed=AARCH64_UNKNOWN_LINUX_GNU_OPENSSL_LIB_DIR AARCH64_UNKNOWN_LINUX_GNU_OPENSSL_LIB_DIR unset cargo:rerun-if-env-changed=OPENSSL_LIB_DIR OPENSSL_LIB_DIR unset cargo:rerun-if-env-changed=AARCH64_UNKNOWN_LINUX_GNU_OPENSSL_INCLUDE_DIR AARCH64_UNKNOWN_LINUX_GNU_OPENSSL_INCLUDE_DIR unset cargo:rerun-if-env-changed=OPENSSL_INCLUDE_DIR OPENSSL_INCLUDE_DIR unset cargo:rerun-if-env-changed=AARCH64_UNKNOWN_LINUX_GNU_OPENSSL_DIR AARCH64_UNKNOWN_LINUX_GNU_OPENSSL_DIR unset cargo:rerun-if-env-changed=OPENSSL_DIR OPENSSL_DIR unset cargo:rerun-if-env-changed=OPENSSL_NO_PKG_CONFIG cargo:rerun-if-env-changed=PKG_CONFIG_ALLOW_CROSS_aarch64-unknown-linux-gnu cargo:rerun-if-env-changed=PKG_CONFIG_ALLOW_CROSS_aarch64_unknown_linux_gnu cargo:rerun-if-env-changed=TARGET_PKG_CONFIG_ALLOW_CROSS cargo:rerun-if-env-changed=PKG_CONFIG_ALLOW_CROSS cargo:rerun-if-env-changed=PKG_CONFIG_aarch64-unknown-linux-gnu cargo:rerun-if-env-changed=PKG_CONFIG_aarch64_unknown_linux_gnu cargo:rerun-if-env-changed=TARGET_PKG_CONFIG cargo:rerun-if-env-changed=PKG_CONFIG cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR_aarch64-unknown-linux-gnu cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR_aarch64_unknown_linux_gnu cargo:rerun-if-env-changed=TARGET_PKG_CONFIG_SYSROOT_DIR cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR run pkg_config fail: pkg-config has not been configured to support cross-compilation. Install a sysroot for the target platform and configure it via PKG_CONFIG_SYSROOT_DIR and PKG_CONFIG_PATH, or install a cross-compiling wrapper for pkg-config and set it via PKG_CONFIG environment variable. --- stderr thread 'main' panicked at ' Could not find directory of OpenSSL installation, and this `-sys` crate cannot proceed without this knowledge. If OpenSSL is installed and this crate had trouble finding it, you can set the `OPENSSL_DIR` environment variable for the compilation process. Make sure you also have the development packages of openssl installed. For example, `libssl-dev` on Ubuntu or `openssl-devel` on Fedora. If you're in a situation where you think the directory *should* be found automatically, please open a bug at https://github.com/sfackler/rust-openssl and include information about your system as well as this message. $HOST = aarch64-apple-darwin $TARGET = aarch64-unknown-linux-gnu openssl-sys = 0.9.97 ', $HOME/.cargo/registry/src/index.crates.io-6f17d22bba15001f/openssl-sys-0.9.97/build/find_normal.rs:190:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
この問題は Cargo Lambda のドキュメントにも実は書かれていた。
自分のケースだと reqwest
クレートが TLS バックエンドとして OpenSSL を使用しているため rustls
feature (あるいは native-tls-vendored
feature)を有効にして、TLS バックエンドをアプリケーションに含めるようにする必要があるらしい。
Cross Compiling with Cargo Lambda | Cargo Lambda
というのも AWS Lambda は Linux サンドボックス環境で関数が実行され、サンドボックスはOSが動作するために必要なライブラリのみ含んでいるため、 *-sys
ライブラリ群はバイナリに完全にリンクされているかネイティブの依存関係を他の方法で提供しない限り動作しない可能性があるためらしい。
そのため自分は Cargo.toml の reqwest
クレートの記述を修正することでビルドを通すことができた。
-reqwest = { version = "0.11", features = ["blocking", "json"] } +reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls"] }
2. Request 構造体のフィールドの DeserializeError
cargo lambda invoke
で payload の key になる値に Request 構造体のフィールド名を一致させる必要がある。
payload の値を受けて serde::Deserialize
するため DeserializeError が発生する。以下は Request 構造体に command というフィールドがあるにもかかわらず空の payload を渡した場合に起こるエラー例。
$ cargo lambda invoke $LAMBDA_FUNCTION_NAME --remote --data-ascii "{}" Error: lambda_runtime::deserializer::DeserializeError × failed to deserialize the incoming data into the function's payload type: missing field `command` at line 1 column 2
もし payload が不要な場合には Request 構造体はフィールドを持たない空の構造体にする。
3. Invoke サブコマンドで Connection refused (os error 61) エラー
前述の通り、すでに AWS Lambda にデプロイされた関数を実行したい場合には、--remote
オプションの指定が必須。
指定しない場合はローカルのエミュレータに対してリクエストを送ってしまい、エミュレータを立ち上げていない場合 Connection refused (os error 61)
エラーが発生してしまう。
$ cargo lambda invoke $LAMBDA_FUNCTION_NAME --output-format json --data-ascii "{}" Error: × error sending request to the runtime emulator ├─▶ error sending request for url (http://[::1]:9000/2015-03-31/functions/$LAMBDA_FUNCTION_NAME/invocations): error trying to connect: tcp connect error: Connection refused (os error 61) ├─▶ error trying to connect: tcp connect error: Connection refused (os error 61) ├─▶ tcp connect error: Connection refused (os error 61) ╰─▶ Connection refused (os error 61)
結果
無事に Rust 製の Lambda 関数をビルド・デプロイできた。
実行の頻度も時間も重くないので、特段気になるところではないが、一応 Billed Duration も Max Memory Used も大きく減った。
Go の Lambda 関数は 600ms, 35MB 程度だったが、Rust だと 400ms, 18MB 程度になっていた*2。
以上。
作業リポジトリ: github.com
*1:ソースコードでの分岐箇所: https://github.com/cargo-lambda/cargo-lambda/blob/a342f0b8a7c34b17cee2f4fed3cc7d3ddbd810ab/crates/cargo-lambda-invoke/src/lib.rs#L156-L160
*2:Go は norpc オプションを有効にしてないなど最適化の余地が残ってるかもしれない