RustでLambda Functionを書いてAWS CDKでデプロイしてみる

せっかくRustを学習しているので、Lambda Functionでも利用してみたくなったのでした。今回はアクセスカウンタのようなものを実装してみようと思います。

関数ハンドラーを記述する

Rust での HTTP イベントの処理SDK for Rustを使用したDynamoDBの例を参考にコーディングします。

なお、参考ページに記載があるように、以下のソースコードで利用されている lambda_http (が依存しているlambda-runtime)は安定版ではないようなので注意が必要そうです。

適当なディレクトリを一つ作成し、cargo initします。

mkdir sample-app
cd sample-app
cargo init

src/main.rs に以下のように書きます(エラーハンドリングは考慮されていません)。

※認証もなく誰でもリクエストできるエンドポイントになっていることに注意します。

use aws_config::BehaviorVersion;
use lambda_http::{run, service_fn, tracing, Body, Error, Request, Response};
use aws_sdk_dynamodb::{Client, types::{AttributeValue, ReturnValue}};

async fn function_handler(_event: Request) -> Result<Response<Body>, Error> {
    // DynamoDB Client
    let config = aws_config::load_defaults(BehaviorVersion::latest()).await;
    let client = Client::new(&config);

    // Atomic Counter をつかったカウンタの更新
    // ref: https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/WorkingWithItems.html#WorkingWithItems.AtomicCounters
    let count = client
        .update_item()
        .table_name("SampleCounterTable")
        .key("counter_name", AttributeValue::S("counter1".to_string()))
        .update_expression("ADD #id :increment")
        .expression_attribute_names("#id", "value")
        .expression_attribute_values(":increment", AttributeValue::N("1".to_string()))
        .return_values(ReturnValue::AllNew)
        .send()
        .await
        .map(|response| {
            response.attributes()
                .unwrap()
                .get("value")
                .unwrap()
                .as_n()
                .unwrap()
                .to_string()
        })
        .expect("failed to update counter");

    let resp = Response::builder()
        .status(200)
        .header("content-type", "text/html")
        .body(format!("Count: {:?}", count).into())
        .expect("failed to render response");

    Ok(resp)
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    tracing::init_default_subscriber();

    run(service_fn(function_handler)).await
}

Cargo.toml の内容は以下のようになります。

[package]
name = "sample-app"
version = "0.1.0"
edition = "2021"

[dependencies]
lambda_http = "0.11.1"
tokio = { version = "1", features = ["macros"] }
aws-sdk-dynamodb = "1.25.0"
aws-config = "1.3.0"

AWS CDKをつかってリソースを定義する

AWS CDKを利用すると、TypeScriptなどでAWSのリソースを定義することができます。 コンストラクトの利用によって、CloudFormationのテンプレートを直接記述するよりも簡単にリソースを定義できるようです。

IaCのためのツールの似たようなものとしてAWS SAMを用いてもよさそうですが、筆者はTypeScriptに馴染みがあるので、今回はAWS CDK Worshopを見ながらAWS CDKを使う方向で進めてみます。

CDK Toolkitをインストールします。

npm install -g aws-cdk

CDKプロジェクトを作成します。

cdk init --language typescript

cargo-lambda/cargo-lambda-cdk と組み合わせて利用すると、CDKを使ってRustコードのコンパイルとLambdaへのデプロイも行ってくれるようになります。

npm i -D cargo-lambda-cdk

cargo-lambda/cargo-lambda-cdkのREADMEにもあるように、Cargo Lambdaをインストールしておく必要があります(筆者はWSLで開発していますが、Homebrewでインストールしました)。 インストールすると、cargoでlambdaサブコマンドが利用できるようになります(AWS CDKによる構成管理をせず、cargo lambda deployコマンド単体でLambda Functionをビルド、デプロイできます)。

>>> % cargo --list

Installed Commands:
    ... (省略) ...
    help                 Displays help for a cargo subcommand
    init                 Create a new cargo package in an existing directory
    install              Install a Rust binary
    lambda
    locate-project       Print a JSON representation of a Cargo.toml file's location
    login                Log in to a registry.
    ... (省略) ...

Cargo.tomlのあるディレクトリで以下のようにCDK関連のスクリプトを格納するディレクトリを作成し、初期化します。

mkdir cdk
cd cdk
cdk init --language typescript

この時点でのディレクトリの状態は以下のとおり。

.
|-- Cargo.lock
|-- Cargo.toml
|-- cdk             <-- いま作ったディレクトリ
|   |-- README.md
|   |-- bin
|   |   `-- cdk.ts
|   |-- cdk.json
|   |-- jest.config.js
|   |-- lib
|   |   `-- cdk-stack.ts
|   |-- package-lock.json
|   |-- package.json
|   |-- test
|   |   `-- cdk.test.ts
|   `-- tsconfig.json
`-- src
    `-- main.rs

以下のように、HTTPリクエストのハンドリングとカウンタロジックのためのLambda Function、 Lambda Functionを公開するためのAPI Gateway, カウンタのストレージとしてDynamoDBを作成します。 テーブルのLambda Functionからのアクセスを許可しておきます。

import { Construct } from 'constructs';
import { RustFunction } from 'cargo-lambda-cdk';
import * as cdk from 'aws-cdk-lib';
import * as apiGateway from 'aws-cdk-lib/aws-apigateway';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';

export class CdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const func = new RustFunction(this, 'SampleFunction', {
      manifestPath: '../Cargo.toml',
    });

    const api = new apiGateway.LambdaRestApi(this, 'SampleEndpoint', {
      handler: func,
    });

    const counterTable = new dynamodb.Table(this, 'SampleCounterTable', {
      tableName: 'SampleCounterTable',
      partitionKey: { name: 'counter_name', type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
    });

    counterTable.grantReadWriteData(func);
  }
}

デプロイする前に、AWS CLIに資格情報がセットされていることを確認します。

以下のコマンドでデプロイします。

cdk deploy

認証情報をプロファイルで管理している場合は、 --profile オプションを利用できます。

cdk deploy --profile my-profile

デプロイが完了すると、エンドポイントのURLがターミナル上で確認できます(https://xxxxxxxxxx.execute-api.region.amazonaws.com/prod/)。

...
Do you wish to deploy these changes (y/n)? y
CdkStack: deploying... [1/1]
CdkStack: creating CloudFormation changeset...

 ✅  CdkStack

✨  Deployment time: 62.46s

Outputs:
CdkStack.xxxxxxxx = https://xxxxxxxxxx.execute-api.region.amazonaws.com/prod/
Stack ARN:
arn:aws:cloudformation:region:xxxxxxxx:stack/CdkStack/xxxx-xxxx-xxxx

✨  Total time: 66.07s

また、CloudFormationのスタックが一つ追加されていることが確認できます。

動作確認してみる

デプロイされたAPIエンドポイントにブラウザからアクセスしてみましょう。アクセスのたびにカウントが更新されます。

カウンタがまわる様子

アプリを削除する

アプリを削除するには cdk destroy コマンドを利用します。DynamoDBのテーブルは以下の操作では削除されないので、別途削除しましょう。

cdk destroy

まとめ

RustでAWS Lambdaにデプロイするための一通りを試してみました。せっかくなので今つくっているアプリケーションの開発に役立てていこうと思います。ではでは〜

このカウンタは @piyoppi/counter-tools を使っています。

クリックすると匿名でいいねできます。