はじめに

    • 基本的に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イメージは使用されているので、場合によってはバージョンが追いついていないということもあるかもしれませんが、現時点ではこれが一番安定しているようです。

結論

リファレンスはちゃんと読もう

bannerAds