[アップデート] AWS SAM CLI で Rust on Lambda をビルドする時に BuildMethod で Cargo Lambda が使えるようになりました

2023.02.27

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

いわさです。

先日 AWS SAM CLI の v1.74.0 がリリースされました。

リリース通知のタイトルにもなっていますが、Rust のビルドがサポート(ベータ)されたというアナウンスがありました。
プログラミング言語ですので Lambda のことだろうと推測出来ますが Lambda のマネージドランタイムで Rust はサポートされていません。また、Amazon Linux 2 カスタムランタイムを使って Rust を Lambda で実行することは以前から出来ました。

では、今回の SAM CLI のアップデートで何が出来るようになったのでしょうか。
今回のリリースに含まれるいくつかのプルリクエストを確認しながら内容を把握し、実際に新しく出来るようになった機能を使ってみましたので紹介します。

What's New アナウンスはコチラ。

BuildMethod で Cargo Lambda がサポートされた

先にまとめです。

AWS SAM のメタデータ BuildMethod で rust-cargolambda が利用出来るようになっています。
BuildMethod は SAM CLI でsam buildを行う際の、対象リソースの処理方法を定義した属性です。

そして、SAM CLI はカスタムランタイムの Rust 用テンプレートが以前から用意されていました。

しかしその時は Makefile を使って独自にビルドする方法でした。 これが、今回の rust-cargolambda を指定することで、Cargo Lambda を使ってビルド出来るようになりました。

試してみる

では SAM CLI の最新版を使ってデプロイして動作確認するところまで試してみたいと思います。
ちなみに今回 sam build を行ったローカル環境は MacBook Pro (Apple M1 Max) です。

sam init でテンプレートを確認

まずはsam initで生成されるテンプレートから確認してみます。
カスタムランタイムを指定した上で、Rust を指定します。

% sam init --runtime provided.al2
Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
        1 - Hello World Example
        2 - Infrastructure event management
        3 - Multi-step workflow
Template: 1

Which runtime would you like to use?
        1 - aot.dotnet7 (provided.al2)
        2 - go (provided.al2)
        3 - graalvm.java11 (provided.al2)
        4 - graalvm.java17 (provided.al2)
        5 - rust (provided.al2)
Runtime: 5
:

SAM テンプレートには次のように BuildMethod が設定されていました。

template.yaml

:
Resources:

:

  PutFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Metadata:
      BuildMethod: rust-cargolambda # More info about Cargo Lambda: https://github.com/cargo-lambda/cargo-lambda
    Properties:
      CodeUri: ./rust_app   # Points to dir of Cargo.toml
      Handler: bootstrap    # Do not change, as this is the default executable name produced by Cargo Lambda
      Runtime: provided.al2
      Architectures:
        - x86_64
:

アプリケーションコード自体はリクエスト内容を解析して DynamoDB へ登録するだけのシンプルな実装です。

rust_app/src/main.rs

use aws_sdk_dynamodb::{model::AttributeValue, Client};
use lambda_http::{service_fn, Body, Error, Request, RequestExt, Response};
use std::env;

/// Main function
#[tokio::main]
async fn main() -> Result<(), Error> {
    // Initialize the AWS SDK for Rust
    let config = aws_config::load_from_env().await;
    let table_name = env::var("TABLE_NAME").expect("TABLE_NAME must be set");
    let dynamodb_client = Client::new(&config);

    // Register the Lambda handler
    //
    // We use a closure to pass the `dynamodb_client` and `table_name` as arguments
    // to the handler function.
    lambda_http::run(service_fn(|request: Request| {
        put_item(&dynamodb_client, &table_name, request)
    }))
    .await?;

    Ok(())
}

:

いや、シンプルな実装ですとか言いましたが Rust のコードは今回初めて見ました。
C++ に近いみたいな噂は聞いてましたけど、たしかになんとなく C++ っぽいので読むだけなら初見でもいけそうです。

どうやら main 関数で次のモジュールを使うことで API Gateway からのリクエスト処理用の Lambda ハンドラを用意しているようです。

間違っていたらスマンという感じです。

ビルドの条件

続いてsam buildをしてみましょう。

冒頭のリリース通知で少し触れられていましたが、今回の機能はベータという位置づけです。
以前 SAM CLI を使って Terraform の関数をローカル実行するベータ機能を紹介したことがあります。

その時と同じようにsam buildでベータ機能を使うフラグを指定する必要があります。

% sam build
Starting Build use cache
Build method "rust-cargolambda" is a beta feature.
Please confirm if you would like to proceed
You can also enable this beta feature with "sam build --beta-features". [y/N]:

今回 Cargo Lambda を使って Rust をビルドするので当然ながらそれらのセットアップが済んでいる必要があります。
以下のエラーが発生した場合は Cargo Lambda がインストールされていない場合です。

% sam build --beta-features

Experimental features are enabled for this session.
Visit the docs page to learn more about the AWS Beta terms https://aws.amazon.com/service-terms/.

Starting Build use cache
Cache is invalid, running build and copying resources for following functions (PutFunction)
Building codeuri: /Users/iwasa.takahito/work/hoge0227rust/hoge0227rust/rust_app runtime: provided.al2 metadata: {'BuildMethod': 'rust-cargolambda'} architecture: x86_64 functions: PutFunction
Running RustCargoLambdaBuilder:CargoLambdaBuild

Build Failed
Error: RustCargoLambdaBuilder:CargoLambdaBuild - Cargo Lambda failed: Cannot find Cargo Lambda. Cargo Lambda must be installed on the host machine to use this feature. Follow the gettings started guide to learn how to install it: https://www.cargo-lambda.info/guide/getting-started.html

ちなみに私は「M1 Mac なんだから x86 で User Container だろう」と思い込んで--use-containerをずっと使っていて、次のようなエラーが発生しうまくいきませんでした。

% sam build --beta-features --use-container

Experimental features are enabled for this session.
Visit the docs page to learn more about the AWS Beta terms https://aws.amazon.com/service-terms/.

Starting Build use cache
Starting Build inside a container
Cache is invalid, running build and copying resources for following functions (PutFunction)
Building codeuri: /Users/iwasa.takahito/work/hoge0227rust/hoge0227rust/rust_app runtime: provided.al2 metadata: {'BuildMethod': 'rust-cargolambda'} architecture: x86_64 functions: PutFunction

Fetching public.ecr.aws/sam/build-provided.al2:latest-x86_64 Docker container image......................................................................................................................................................................................................................................................................................................................................................................................................................
Mounting /Users/iwasa.takahito/work/hoge0227rust/hoge0227rust/rust_app as /tmp/samcli/source:ro,delegated inside runtime container

Build Failed
Error: RustCargoLambdaBuilder:Resolver - Path resolution for runtime: provided of binary: cargo was not successful

Rust (cargo) と Cargo Lambda のクロスコンパイル機能にお任せで良いみたいです。

実行

sam buildが出来たらあとは通常どおりsam deployします。
テンプレートを見ると次のように API Gateway と DynamoDB をも作成されそうです。

template.yaml

:
Resources:
  Table:
    Type: AWS::Serverless::SimpleTable # More info about SimpleTable Resource: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-simpletable.html
    Properties:
      PrimaryKey:
        Name: id
        Type: String

  PutFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Metadata:
      BuildMethod: rust-cargolambda # More info about Cargo Lambda: https://github.com/cargo-lambda/cargo-lambda
    Properties:
:
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /{id}
            Method: put
      Environment:
        Variables:
          TABLE_NAME: !Ref Table
      Policies:
        - DynamoDBWritePolicy: # More info about SAM policy templates: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-policy-templates.html
            TableName: !Ref Table
:

デプロイ後に実際 API Gateway リソースも確認してみましたがパスパラメータで適当な ID を指定して PUT すれば良いようです。

また、以下のコードからするとリクエストボディの内容をそのまま payload 値として登録してくれそうなので、適当な文字列を PUT リクエストの Body にセットしてみます。

main.rs

:

async fn put_item(
    client: &Client,
    table_name: &str,
    request: Request,
) -> Result<Response<Body>, Error> {
    // Extract path parameter from request
    let path_parameters = request.path_parameters();
    let id = match path_parameters.first("id") {
        Some(id) => id,
        None => return Ok(Response::builder().status(400).body("id is required".into())?),
    };

    // Extract body from request
    let body = match request.body() {
        Body::Empty => "".to_string(),
        Body::Text(body) => body.clone(),
        Body::Binary(body) => String::from_utf8_lossy(body).to_string(),
    };

    // Put the item in the DynamoDB table
    let res = client
        .put_item()
        .table_name(table_name)
        .item("id", AttributeValue::S(id.to_string()))
        .item("payload", AttributeValue::S(body))
        .send()
        .await;

    // Return a response to the end-user
    match res {
        Ok(_) => Ok(Response::builder().status(200).body("item saved".into())?),
        Err(_) => Ok(Response::builder().status(500).body("internal error".into())?),
    }
}

:

こんな感じだろうか。

% curl -X PUT -d "hogehogehoge" "https://qa5ovyp5nd.execute-api.ap-northeast-1.amazonaws.com/Prod/111/"              
item saved
% curl -X PUT -d "fugafugafuga" "https://qa5ovyp5nd.execute-api.ap-northeast-1.amazonaws.com/Prod/222/"
item saved

登録出来ていそうです。
DynamoDB のテーブルをスキャンします。テーブル名はsam deployの出力から取得しました。

% aws dynamodb scan --table-name hoge0227rust-Table-92EBA75A5UAY
{
    "Items": [
        {
            "payload": {
                "S": "fugafugafuga"
            },
            "id": {
                "S": "222"
            }
        },
        {
            "payload": {
                "S": "hogehogehoge"
            },
            "id": {
                "S": "111"
            }
        }
    ],
    "Count": 2,
    "ScannedCount": 2,
    "ConsumedCapacity": null
}

登録されていますね。なるほど。

さいごに

本日は AWS SAM CLI の BuildMethod で Cargo Lambda がサポートされたようだったので実際に試してみました。

SAM CLI で Rust を動かしている方は是非今回のアップデートを試してみてください。
また、本機能はベータ機能になります。この記事は本日時点で情報で作成されていますが、今後変更や廃止の可能性もありますのでその点は気にしておきましょう。