cargo-lambda + cargo-lambda-cdkでRustで書いたLambdaをAWSにデプロイするハンズオン記事

## はじめに

LambdaをRustで書く機会がありました。
LambdaはRustを標準でサポートしているわけではないので、通常であればカスタムランタイムを使うことになると思います。
一見大変そう...と感じるかもしれませんが、cargo-lambdaとcargo-lambda-cdkを用いることによって、快適に開発を進められました。

今回の記事では簡単なAPIをハンズオンで実装することでそのtipsをお伝えできればと思っています。
本記事では,POSTすることでDynamoDBにレコード追加をするLambda, 
GETすることで作成したレコードの一覧を返すLambdaの2つをcargo-lambda, cargo-lambda-cdkを用いて定義・デプロイしたいと思います。

## cargo-lambda, cargo-lambda-cdkとは？

### cargo-lambdaとは

https://github.com/cargo-lambda/cargo-lambda

ひとことで言ってしまえばRust + Lambda開発のためのcargoのサブコマンドです。
cargo-lambdaの嬉しいところは`/bin`の中にバイナリファイル複数作っておけばそれごとにlambdaを作成できるのでモジュールの再利用等がしやすい点です。
また、`cargo lambda watch`コマンドで、lambdaをローカルで実行できる点も非常に嬉しいです。
一般的なCDK + Lambdaの開発だとLambdaのローカル実行はsam localやlocalstackを使用することになると思いますがコマンド１個ですぐにローカル実行できるのは便利です。

今回の記事でもこれらの点を含めてご紹介できればと思っています。

### cargo-lambda-cdkとは

https://github.com/cargo-lambda/cargo-lambda-cdk

cargo-lambda単体でも十分便利なのですが、たとえばS3を作成する、DynamoDBを作成するなどといったリソース付与だったり、IAMロールの付与だったりができません。
この辺はCDKだったりserverless frameworkだったりのいわゆるIaCツールが得意なところですので、ちゃっちゃとやってしまいたいところです。
ですが、Lambdaをカスタムランタイムを設定しなくてはいけないしなんだかめんどくさそう...
↑を解決してくれるのが`cargo-lambda-cdk`というnpm packageです。
こちらを用いることでCDKで`RustFunction`というモジュールを定義できるようになり、
以下の様に簡単にLambdaを定義できるようになります。

```ts
    const lambda = new RustFunction(this, 'sample-lambda', {
      manifestPath: path.join(__dirname, '..', 'lambda', 'Cargo.toml'),
      binaryName: 'lambda1',
      vpc: vpc,
      vpcSubnets: {
        subnets: config.lambda.subnets.map((subnet, idx) => Subnet.fromSubnetId(this, `subnet-${idx}`, subnet))
      },
      allowPublicSubnet: false,
      environment: {
        REGION: config.region,
      },
    })
```


## インストール

- Rust, cargo

https://www.rust-lang.org/ja/tools/install

- aws-cdk

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/getting_started.html

初めて設定する方は`aws configure`で認証情報を設定してください。

- cargo-lambda

https://www.cargo-lambda.info/guide/installation.html

## DynamoDBテーブル・作成API定義

さて、今回実装するAPIで操作するDynamoDBのテーブルを定義します。
今回はここが本質ではないので非常に簡易なテーブルとしました。
PKをname, SKをcreatedAtとして、追加でmessageを持つようなテーブルとします。

また、レコード作成のリクエストはnameとmessageをrequest bodyに入れてPOSTリクエストするようにします。
つまりはput-recordのリクエストの想定は以下のような感じです。

```
$ http POST <endpoint> name=haw_ohnuma message=hoge
```

GETに関しては全てのレコードを返すだけなのでただ単にエンドポイントにリクエストを投げるだけにします。

```
$ http <endpoint>
```

### Lambda

| lambda      | method | 処理                         |
| ----------- | ------ | -------------------------- |
| put-record  | POST   | DynamoDBにリクエストされたレコードを追加する |
| get-records | GET    | DynamoDBのレコード一覧を返す         |
### DynamoDB

| key       | 型      | 設定例                              |
| --------- | ------ | -------------------------------- |
| name(PK)  | String | haw_ohnuma                       |
| createdAt | String | 2024-09-27T13:23:55.941840+09:00 |
| message   | String | hello cargo lambda               |
## CDKリポジトリ作成 & cargo-lambda-cdk導入

各種インストールが済んだら`cdk init`しましょう。
`cargo-lambda-cdk`がnpm packageのため、CDKの言語はjsかtsを選択してください。
自分はtypescriptを選択します。

```
$ mkdir sample-rust-api && cd sample-rust-api
$ cdk init sample-app --language typescript
```

initが終わったらついでに`cargo-lambda-cdk`をインストールしちゃいましょう。

```
$ npm i cargo-lambda-cdk
```

これで準備は整いました。
cargo-lambdaを使ってRustでLambdaを実装していきましょう！

## cargo-lambdaでのLambdaの実装

### cargo lambda new

Lambdaの作業環境を作ります。今回はCDKリポジトリ内の`lambda`というディレクトリを作成します。
するとHTTP Functionを作るのか聞かれます。今回はAPI開発ですのでYesで作成します。

```
$ cargo lambda new lambda
> Is this function an HTTP function? Yes
```

すると、新しいcargo PJが`/lambda`に作成されます。

```
$ tree lambda
lambda
├── Cargo.lock
├── Cargo.toml
└── src
    └── main.rs
```
これでプロジェクトの初期設定は以上となります。

### (tips)初期生成コードのローカル実行

`src/main.rs`に初期生成コードが作成されています。
自分の環境では以下のようなコードが生成されていました。

```rust
use lambda_http::{run, service_fn, tracing, Body, Error, Request, RequestExt, Response};

/// 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
async fn function_handler(event: Request) -> Result<Response<Body>, Error> {
    // Extract some useful information from the request
    let who = event
        .query_string_parameters_ref()
        .and_then(|params| params.first("name"))
        .unwrap_or("world");
    let message = format!("Hello {who}, this is an AWS Lambda HTTP request");

    // Return something that implements IntoResponse.
    // It will be serialized to the right response event automatically by the runtime
    let resp = Response::builder()
        .status(200)
        .header("content-type", "text/html")
        .body(message.into())
        .map_err(Box::new)?;
    Ok(resp)
}

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

    run(service_fn(function_handler)).await
}
```

何をやっているのでしょうか？
これをローカルで動かしてみましょう。

```
$ cd lambda
$ cd cargo lambda watch
 INFO invoke server waiting for requests socket_addr=[::]:9000
 INFO starting lambda function function="_" manifest="Cargo.toml" cmd=Exec { prog: "cargo", args: ["run", "--manifest-path", "Cargo.toml", "--color", "auto"] }
	Compiling pin-project-lite v0.2.14
	Compiling itoa v1.0.11
	Compiling memchr v2.7.4
	Compiling futures-core v0.3.30
	Compiling futures-sink v0.3.30
	Compiling slab v0.4.9

		.....
	 
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
     Running `target/debug/lambda`

```

するとローカルサーバーがポート9000番に立ち上がります。

curlでもpostmanでもなんでもいいのでリクエストしてみましょう。
自分はHTTPieを使っています。

https://httpie.io/

```
$ http :9000

Hello world, this is an AWS Lambda HTTP request
```

Hello worldが返ってきました。コードをよく見ると、

```rust
    let who = event
        .query_string_parameters_ref()
        .and_then(|params| params.first("name"))
        .unwrap_or("world");
    let message = format!("Hello {who}, this is an AWS Lambda HTTP request");
```


これが返されていることがわかります。さきほどのリクエストではnameに何も値を入れなかったのでunwrap_orの値が返されていました。
そこで、query string parameterにnameを設定してリクエストしてみましょう。

```
$ http :9000 name==haw_ohnuma

Hello haw_ohnuma, this is an AWS Lambda HTTP request
```

正しくローカルで実行できていそうです。
こんな簡単にlambdaの挙動をローカルで検証できました。
### put-record API実装

それではDynamoDBへのリクエスト処理を実装していきます。

まずはAWS SDKのクレートを導入していきます。
Cargo.tomlに下記を追加して`cargo build`してください。

```toml
[dependencies]
lambda_http = "0.13.0"

tokio = { version = "1", features = ["macros"] }
aws-config = { version = "1", features = ["behavior-version-latest"] }
aws-sdk-dynamodb = "1"
serde_dynamo = { version = "4", features = ["aws-sdk-dynamodb+1"] }
```

ちなみにですがAWS SDK for Rustは2023年11月にめでたくGAされました。安心して使えますね。

https://aws.amazon.com/jp/about-aws/whats-new/2023/11/aws-sdk-rust/

また、Event Bodyのパースのためにserde, createdAt取得のためにchronoエラー処理のためにanyhowを用います。
それぞれCargo.tomlに追加しましょう。

```toml
[dependencies]
lambda_http = "0.13.0"

tokio = { version = "1", features = ["macros"] }
aws-config = { version = "1", features = ["behavior-version-latest"] }
aws-sdk-dynamodb = "1"
serde_dynamo = { version = "4", features = ["aws-sdk-dynamodb+1"] }
serde = {version = "1.0", features = ["derive"]}
serde_json = "1.0"
anyhow = { version = "1.0.70", features = ["backtrace", "std"] }
chrono = "0.4"
chrono-tz = "0.6"

```

まずは、put-recordのエントリポイント用に`lambda/src/bin/put-record.rs`を作成してください。
また、共用モジュールの一括管理のために、lambda/src/lib.rsを作成しておいてください。
treeでみるとこんな感じです。

```
$ tree -I target ../lambda
  
../lambda
├── Cargo.lock
├── Cargo.toml
└── src
    ├── bin
    │   └── put-record.rs
    └── lib.rs
```

それではput-record.rsに以下を追加してください。

```rust
use std::collections::HashMap;

use anyhow::{anyhow, Result};
use aws_config::SdkConfig;
use aws_sdk_dynamodb::{config::Credentials, Client};
use chrono::Utc;
use chrono_tz::Asia::Tokyo;
use lambda::{format_json_response, DynamoItem};
use lambda_http::{run, service_fn, tracing, Body, Error, Request, Response};
use serde::{Deserialize, Serialize};
use serde_dynamo::aws_sdk_dynamodb_1::to_item;
use serde_json::json;

#[derive(Deserialize, Serialize)]
struct EventBody {
    name: String,
    message: String,
}

fn get_current_datetime_in_tokyo() -> String {
    let now_utc = Utc::now();
    let now_tokyo = now_utc.with_timezone(&Tokyo);
    now_tokyo.to_rfc3339()
}

fn parse_event_body(body: &Body) -> Result<EventBody> {
    let str_body = match body {
        Body::Text(text) => text,
        _ => return Err(anyhow!("invalid event body format")),
    };
    let parsed_body: EventBody = serde_json::from_str(str_body)?;
    Ok(parsed_body)
}

async fn get_aws_clinet_config(is_offline: &str) -> SdkConfig {
    match is_offline {
        "true" => {
            let credentials = Credentials::new(
                "aws_access_key_id",
                "aws_secret_access_key",
                None,
                None,
                "offline",
            );
            aws_config::from_env()
                .endpoint_url("http://localhost:8000")
                .credentials_provider(credentials)
                .load()
                .await
        }
        "false" => aws_config::load_from_env().await,
        _ => unreachable!("failed to get is_offline flag"),
    }
}

async fn function_handler(event: Request) -> Result<Response<Body>> {
    let envs = std::env::vars().collect::<HashMap<_, _>>();
    let is_offline = envs
        .get("IS_OFFLINE")
        .unwrap_or(&String::from("false"))
        .to_owned();
    let table_name = envs
        .get("TABLE_NAME")
        .unwrap()
        .to_owned();
    let config = get_aws_clinet_config(&is_offline).await;
    let client = Client::new(&config);
    let body = event.body();
    let event_body = parse_event_body(body)?;
    let current_date_time = get_current_datetime_in_tokyo();
    let item: DynamoItem = DynamoItem {
        name: event_body.name,
        message: event_body.message,
        date: current_date_time,
    };
    client
        .put_item()
        .table_name(table_name)
        .set_item(Some(to_item(item).unwrap()))
        .send()
        .await?;

    format_json_response(200, &json!({"result": "ok"}).to_string())
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing::init_default_subscriber();
    run(service_fn(|event| async move {
        function_handler(event).await.map_err(Error::from)
    }))
    .await
    .map_err(|e| anyhow!(e.to_string()))
}

```

そして、`lib.rs`に以下を実装します。

```rust
use anyhow::Result;
use lambda_http::{http::StatusCode, Body, Response};
use serde::{Deserialize, Serialize};

pub fn format_json_response(status_code: u16, body: &str) -> Result<Response<Body>> {
    let resp = Response::builder()
        .status(StatusCode::from_u16(status_code).unwrap())
        .header("content-type", "application/json")
        .body(body.into())
        .map_err(Box::new)?;
    Ok(resp)
}

#[derive(Deserialize, Serialize)]
pub struct DynamoItem {
    pub name: String,
    pub date: String,
    pub message: String,
}
```

急に完成物を見せてしまいましたが、特段難しいことはしていません。
handlerの上から順にお気持ちを書いておきます。

#### Event Body

```rust
#[derive(Deserialize, Serialize)]
struct EventBody {
    name: String,
    message: String,
}

fn parse_event_body(body: &Body) -> Result<EventBody> {
    let str_body = match body {
        Body::Text(text) => text,
        _ => return Err(anyhow!("invalid event body format")),
    };
    let parsed_body: EventBody = serde_json::from_str(str_body)?;
    Ok(parsed_body)
}


...


    let body = event.body();
    let event_body = parse_event_body(body)?;

```

今回はリクエストボディにnameとmessageを与えてリクエストする想定ですので、
その構造体を定義してあげて、serdeでパースしてあげます。
#### config取得箇所

```rust
async fn get_aws_clinet_config(is_offline: &str) -> SdkConfig {
    match is_offline {
        "true" => {
            let credentials = Credentials::new(
                "aws_access_key_id",
                "aws_secret_access_key",
                None,
                None,
                "offline",
            );
            aws_config::from_env()
                .endpoint_url("http://localhost:8000")
                .credentials_provider(credentials)
                .load()
                .await
        }
        "false" => aws_config::load_from_env().await,
        _ => unreachable!("failed to get is_offline flag"),
    }
}
```

aws clientのconfigを掴む関数です。
ローカル実行とそうでないときの条件分岐を簡単にしたかったので、こんな関数を用意しています。
環境変数に設定された`IS_OFFLINE`が"true"になっていた場合に設定したcredential情報を持って、endpoint_urlに設定されたエンドポイントへリクエストを投げます。
このような関数を用意しておくことで、

```
$ IS_OFFLINE=true cargo lambda watch
```

とコマンドを叩くことで簡単にローカルへリクエストを飛ばせるということです。

#### DynamoDBへのリクエスト

```rust
use serde_dynamo::aws_sdk_dynamodb_1::to_item;

    let item: DynamoItem = DynamoItem {
        name: event_body.name,
        message: event_body.message,
        date: current_date_time,
    };
    client
        .put_item()
        .table_name(table_name)
        .set_item(Some(to_item(item).unwrap()))
        .send()
        .await?;
```

Rustの型堅牢さゆえに通常putItemを実行するのはちまちまと一つ一つのキーを指定して書かなくてはならないのですが、
`serde_dynamo`という偉大なるクレートのおかげでだいぶ簡単に書くことができます。

ここでは詳しく説明しませんがみんな大好きクラメソさんが記事にしてくれているので気になる方は以下の記事をご覧ください。

https://dev.classmethod.jp/articles/use-dynamodb-with-serde-dynamodb/

### 作成物のローカル実行

それでは作ったコードをローカルで実行して正しく動作することを確認してみましょう。
DynamoDBのローカルでのエミュレータはDynamoDB localを用います。

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DynamoDBLocal.html


`docker-compose.yml`を作成して下記を追加してください。
これで先ほど`IS_OFFLINE`がtrueの時に`endpoint_url`に指定した`http://localhost:8000`にDynamoDBを構えることができます。

```yml
services:
  dynamodb:
    image: amazon/dynamodb-local:1.21.0
    ports:
      - 8000:8000
    volumes:
      - ./.storage/dynamodb:/data

volumes:
  dynamodb:
```

 ローカル実行でのAWSのクレデンシャル設定をするため、`~/.aws/config`に以下を追加します。

```
[profile local]
aws_access_key_id = aws_access_key_id
aws_secret_access_key = aws_secret_access_key
region = ap-northeast-1
```

次に、テーブルを作成します。
実際にデプロイする際はこの辺の定義はCDKで行なってしまうので今回は若干めんどくさいですがCLIで以下のコマンドを叩きます。

```
$ aws dynamodb create-table \
	--table-name sample-rust-api-table-local \
	--attribute-definitions \
			AttributeName=name,AttributeType=S \
			AttributeName=date,AttributeType=S \
	--key-schema \
			AttributeName=name,KeyType=HASH \
			AttributeName=date,KeyType=RANGE \
	--billing-mode PAY_PER_REQUEST --endpoint-url http://localhost:8000 --profile local --region ap-northeast-1

{
    "TableDescription": {
        "AttributeDefinitions": [
            {
                "AttributeName": "name",
                "AttributeType": "S"
            },
            {
                "AttributeName": "date",
                "AttributeType": "S"
            }
        ],
        "TableName": "sample-rust-api-table-local",
        "KeySchema": [
            {
                "AttributeName": "name",
                "KeyType": "HASH"
            },
            {
                "AttributeName": "date",
                "KeyType": "RANGE"
            }
        ],
        "TableStatus": "ACTIVE",
        "CreationDateTime": "2024-09-27T13:23:26.221000+09:00",
        "ProvisionedThroughput": {
            "LastIncreaseDateTime": "1970-01-01T09:00:00+09:00",
            "LastDecreaseDateTime": "1970-01-01T09:00:00+09:00",
            "NumberOfDecreasesToday": 0,
            "ReadCapacityUnits": 0,
            "WriteCapacityUnits": 0
        },
        "TableSizeBytes": 0,
        "ItemCount": 0,
        "TableArn": "arn:aws:dynamodb:ddblocal:000000000000:table/sample-rust-api-table-local",
        "BillingModeSummary": {
            "BillingMode": "PAY_PER_REQUEST",
            "LastUpdateToPayPerRequestDateTime": "2024-09-27T13:23:26.221000+09:00"
        }
    }
}
```

テーブルが作成できたので以下のコマンドでローカル起動して、リクエストしてみましょう。
/binにエントリポイントを定義した場合には`:9000/lambda-url/<filename>`でリクエストします。

```
$ IS_OFFLINE=true TABLE_NAME=sample-rust-api-table-local cargo lambda watch
$ http POST :9000/lambda-url/put-record name=haw_ohnuma message=hoge
```

```json
{
    "result": "ok"
}
```

が返ってくれば成功です。ちゃんとputできているかscanして確認してみます。

```
$ aws dynamodb scan --table-name sample-rust-api-table-local --endpoint-url http://localhost:8000 --region ap-northeast-1 --profile local
```

```json
{
    "Items": [
        {
            "date": {
                "S": "2024-09-27T14:26:52.607810+09:00"
            },
            "message": {
                "S": "hoge"
            },
            "name": {
                "S": "haw_ohnuma"
            }
        }
    ],
    "Count": 1,
    "ScannedCount": 1,
    "ConsumedCapacity": null
}
```

正しくputができていそうです！

### get-records API実装

それでは次もサクッと実装してしまいましょう。
`bin/get-records.rs`を作成して以下を追加してください。

```rust
use std::collections::HashMap;

use anyhow::{anyhow, Result};
use aws_config::SdkConfig;
use aws_sdk_dynamodb::{config::Credentials, Client};
use lambda::{format_json_response, DynamoItem};
use lambda_http::{run, service_fn, tracing, Body, Error, Request, Response};
use serde_dynamo::aws_sdk_dynamodb_1::from_item;
use serde_json::json;


async fn get_aws_clinet_config(is_offline: &str) -> SdkConfig {
    match is_offline {
        "true" => {
            let credentials = Credentials::new(
                "aws_access_key_id",
                "aws_secret_access_key",
                None,
                None,
                "offline",
            );
            aws_config::from_env()
                .endpoint_url("http://localhost:8000")
                .credentials_provider(credentials)
                .load()
                .await
        }
        "false" => aws_config::load_from_env().await,
        _ => unreachable!("failed to get is_offline flag"),
    }
}

async fn function_handler(_event: Request) -> Result<Response<Body>> {
    let envs = std::env::vars().collect::<HashMap<_, _>>();
    let is_offline = envs
        .get("IS_OFFLINE")
        .unwrap_or(&String::from("false"))
        .to_owned();
    let table_name = envs.get("TABLE_NAME").unwrap().to_owned();
    let config = get_aws_clinet_config(&is_offline).await;
    let client = Client::new(&config);
    let output = client.scan().table_name(table_name).send().await.unwrap();
    let items = output
        .items
        .unwrap()
        .into_iter()
        .map(|item| {
            let dynamo_item: DynamoItem = from_item(item).unwrap();
            dynamo_item
        })
        .collect::<Vec<DynamoItem>>();

    format_json_response(200, &json!({"items": items}).to_string())
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing::init_default_subscriber();
    run(service_fn(|event| async move {
        function_handler(event).await.map_err(Error::from)
    }))
    .await
    .map_err(|e| anyhow!(e.to_string()))
}

```


こちらのコードに関しては先ほどのコードで説明した以上のお気持ち解説がありませんでした...
`from_item(item)`で`HashMap<String, AttributeValue>`を`DynamoItem`にパースしてくれています。serde_dynamoありがたや...

### 作成物のローカル実行

こちらは全レコードの出力だったので特段リクエストに何かを込めるみたいなことはしてないのでそのまま実行します。
せっかくなので、先ほど作ったputのAPIで少しレコードを増やしてから実行しています。

```
$ http POST :9000/lambda-url/get-records
```

```json
{
    "items": [
        {
            "date": "2024-09-27T14:47:17.549288+09:00",
            "message": "piyo",
            "name": "haw_ohnuma3"
        },
        {
            "date": "2024-09-27T14:47:06.981753+09:00",
            "message": "fuga",
            "name": "haw_ohnuma2"
        },
        {
            "date": "2024-09-27T14:26:52.607810+09:00",
            "message": "hoge",
            "name": "haw_ohnuma"
        }
    ]
}
```

いい感じに取得できています！
簡単ではありましたが、Rustを使ったLambdaの実装は以上になります。

## CDKでAWSリソースの定義

さて、最後にAWSリソースの定義をCDKで宣言して先ほど実装したLambdaを実際にAWSにデプロイしましょう。
ここからはRustは使わず、Typescriptでの記述になります。

一旦lambdaディレクトリから抜けて、CDKリポジトリ直下に移動しましょう。

`lib/sample-rust-api-stack.ts`にリソースを定義していきましょう！

```ts
import { Duration, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';
import { LambdaIntegration, RestApi } from 'aws-cdk-lib/aws-apigateway';
import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb';
import { RustFunction } from 'cargo-lambda-cdk';
import { Construct } from 'constructs';
import * as path from 'path'

export class SampleRustApiStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
    const prefix = 'sample-rust-api'
    const tableName = `${prefix}-table`

    // Lambdaの定義
    const putRecordLambda = new RustFunction(this, 'put-record', {
      functionName: `${prefix}-put-record`,
      manifestPath: path.join(__dirname, '..', 'lambda', 'Cargo.toml'),
      binaryName: 'put-record',
      environment: {
        TABLE_NAME: tableName
      },
    })
    const getRecordsLambda = new RustFunction(this, 'get-records', {
      functionName: `${prefix}-get-records`,
      manifestPath: path.join(__dirname, '..', 'lambda', 'Cargo.toml'),
      binaryName: 'get-records',
      environment: {
        TABLE_NAME: tableName
      },
    })

    // DynamoDBの定義
    const table = new Table(this, tableName, {
      tableName: tableName,
      partitionKey: {
        name: 'name',
        type: AttributeType.STRING,
      },
      sortKey: {
        name: 'date',
        type: AttributeType.STRING,
      },
      billingMode: BillingMode.PAY_PER_REQUEST,
      removalPolicy: RemovalPolicy.DESTROY,
    });

    // Lambdaに対してテーブルの読み書きロール付与
    table.grantReadWriteData(putRecordLambda)
    table.grantReadWriteData(getRecordsLambda)

    // API Gatewayの定義
    const apiGateway = new RestApi(this, "api-gateway", {
      restApiName: `${prefix}-apigateway`,
    });

    const apiPath = apiGateway.root;
    // rootのPOSTにputRecordLambdaを紐付け
    apiPath.addMethod(
      "POST",
      new LambdaIntegration(putRecordLambda, {
        allowTestInvoke: false,
      }),
    );

    // rootのGETにputRecordLambdaを紐付け
    apiPath.addMethod(
      "GET",
      new LambdaIntegration(getRecordsLambda, {
        allowTestInvoke: false,
      }),
    );
  }
}
```


### Lambda
ここでやっと`cargo-lambda-cdk`の出番です。

```ts
import { RustFunction } from 'cargo-lambda-cdk';

...

    const putRecordLambda = new RustFunction(this, 'put-record', {
      functionName: `${prefix}-put-record`,
      manifestPath: path.join(__dirname, '..', 'lambda', 'Cargo.toml'),
      binaryName: 'put-record',
      environment: {
        TABLE_NAME: tableName
      },
    })
```

`manifestPath`に作成したCargo.tomlのパスを指定してあげます。
また、`binaryName`にこのLambdaにデプロイするファイルを指定してあげます。
これで、このLambdaは先ほど実装したコードの`bin/put-record.rs`をエントリポイントとして定義できるというわけです。
これだけ？と思うかもしれませんが、たったこれだけでカスタムランタイムのRustを定義できてしまうのが`cargo-lambda-cdk`の魅力です！

### DynamoDB

```ts
    const table = new Table(this, tableName, {
      tableName: tableName,
      partitionKey: {
        name: 'name',
        type: AttributeType.STRING,
      },
      sortKey: {
        name: 'date',
        type: AttributeType.STRING,
      },
      billingMode: BillingMode.PAY_PER_REQUEST,
      removalPolicy: RemovalPolicy.DESTROY,
    });

    // Lambdaに対してテーブルの読み書きロール付与
    table.grantReadWriteData(putRecordLambda)
    table.grantReadWriteData(getRecordsLambda)
```

DynamoDBの宣言です。
PKにname, SKにdateをそれぞれ文字列型で宣言しています。

また`removalPolicy`に`DESTROY`を設定しないとCloudFormation Stackを削除したときに合わせてテーブルを削除してくれません。
今回はハンズオンのサンプルStackですので後片付けしやすいようにこちらを設定しています。

`grantReadWriteData`でこのDBへの読み書き許可ロールをLambdaに付与してあげています。
たったこれだけの記述でロールが付与できるところがCDKが好きな理由の一つです。

### API Gateway

```ts
    const apiGateway = new RestApi(this, "api-gateway", {
      restApiName: `${prefix}-apigateway`,
    });

    const apiPath = apiGateway.root;
    // rootのPOSTにputRecordLambdaを紐付け
    apiPath.addMethod(
      "POST",
      new LambdaIntegration(putRecordLambda, {
        allowTestInvoke: false,
      }),
    );

    // rootのGETにputRecordLambdaを紐付け
    apiPath.addMethod(
      "GET",
      new LambdaIntegration(getRecordsLambda, {
        allowTestInvoke: false,
      }),
    );

```

API Gatewayのルートにメソッドを定義しています。
また、そのメソッドに`LambdaIntegration`を使ってLambda統合をひっかけています。
このようにしてrootのPOSTメソッドが呼ばれることで`putRecordLambda`を発火できるようにしています。

ここまでできればリソースの準備も完了です！デプロイしましょう。

```
$ CDK_REGION=ap-northeast-1 npm run cdk deploy
```

正しくデプロイが終了すれば完了です！
## 動作確認

マネコンに移動してLambdaが正しくデプロイできているか確認してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3622760/6078c161-5325-f341-55d5-bbb1eb5b6a2d.png)

カスタムランタイムのLambdaが生まれていることがわかります！
また、DynamoDBテーブルも確認しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3622760/8b534424-3f40-b595-686d-298740dd56f8.png)

パーティションキー、ソートキーも正しく設定され、デプロイできていそうですね。

ここまで確認できればAPI GatewayのエンドポイントURLを取得して、APIにリクエストをしてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3622760/dc0934ca-02c6-4e54-3f27-d2d1e373b11a.png)

```
$ http POST <endpoint> name=haw_ohnuma message="hello world from cargo lambda"
$ http <endpoint>
```

```json
{
    "items": [
        {
            "date": "2024-09-27T15:17:16.933958463+09:00",
            "message": "cargo-lambdaとcargo-lambda-cdkを使ってDynamoDBにレコードを格納できました！",
            "name": "haw_ohnuma2"
        },
        {
            "date": "2024-09-27T15:15:47.074315362+09:00",
            "message": "hello world from cargo lambda",
            "name": "haw_ohnuma"
        },
        {
            "date": "2024-09-27T15:17:48.993830117+09:00",
            "message": "hogehogehogehogeho",
            "name": "haw_ohnuma"
        }
    ]
}

```

実際にマネコンからもDynamoDBレコードが作成されていることを確認できます！
正しく動作していそうです！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3622760/352638b2-1aaf-8167-d10e-f700513814cd.png)



ここまでできれば今回のcargo-lambda, cargo-lambda-cdkを使ったRustを使ったLambdaのAPI開発はおしまいです。ありがとうございました！

## 後片付け

マネコンのCloudFormationからStackの削除を実行することでまとめて消せます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3622760/12cfdbd0-6d23-d2f6-6fa0-9d2a3511298d.png)


## おわりに

cargo-lambda, cargo-lambda-cdkを用いたRustでのLambda開発の流れをハンズオン形式で紹介してみました。
誰かのお役に立てる記事となれば幸いです。
もっとこうしたほうがイイ！こここう書ける！みたいなことあったらコメントにて教えてください。