はじめに
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とは
ひとことで言ってしまえばRust + Lambda開発のためのcargoのサブコマンドです。
cargo-lambdaの嬉しいところは/bin
の中にバイナリファイル複数作っておけばそれごとにlambdaを作成できるのでモジュールの再利用等がしやすい点です。
また、cargo lambda watch
コマンドで、lambdaをローカルで実行できる点も非常に嬉しいです。
一般的なCDK + Lambdaの開発だとLambdaのローカル実行はsam localやlocalstackを使用することになると思いますがコマンド1個ですぐにローカル実行できるのは便利です。
今回の記事でもこれらの点を含めてご紹介できればと思っています。
cargo-lambda-cdkとは
cargo-lambda単体でも十分便利なのですが、たとえばS3を作成する、DynamoDBを作成するなどといったリソース付与だったり、IAMロールの付与だったりができません。
この辺はCDKだったりserverless frameworkだったりのいわゆるIaCツールが得意なところですので、ちゃっちゃとやってしまいたいところです。
ですが、Lambdaをカスタムランタイムを設定しなくてはいけないしなんだかめんどくさそう...
↑を解決してくれるのがcargo-lambda-cdk
というnpm packageです。
こちらを用いることでCDKでRustFunction
というモジュールを定義できるようになり、
以下の様に簡単にLambdaを定義できるようになります。
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
- aws-cdk
初めて設定する方はaws configure
で認証情報を設定してください。
- cargo-lambda
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
に初期生成コードが作成されています。
自分の環境では以下のようなコードが生成されていました。
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を使っています。
$ http :9000
Hello world, this is an AWS Lambda HTTP request
Hello worldが返ってきました。コードをよく見ると、
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
してください。
[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されました。安心して使えますね。
また、Event Bodyのパースのためにserde, createdAt取得のためにchronoエラー処理のためにanyhowを用います。
それぞれCargo.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に以下を追加してください。
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
に以下を実装します。
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
#[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取得箇所
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へのリクエスト
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
という偉大なるクレートのおかげでだいぶ簡単に書くことができます。
ここでは詳しく説明しませんがみんな大好きクラメソさんが記事にしてくれているので気になる方は以下の記事をご覧ください。
作成物のローカル実行
それでは作ったコードをローカルで実行して正しく動作することを確認してみましょう。
DynamoDBのローカルでのエミュレータはDynamoDB localを用います。
docker-compose.yml
を作成して下記を追加してください。
これで先ほどIS_OFFLINE
がtrueの時にendpoint_url
に指定したhttp://localhost:8000
にDynamoDBを構えることができます。
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
{
"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
{
"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
を作成して以下を追加してください。
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
{
"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
にリソースを定義していきましょう!
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
の出番です。
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
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
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が正しくデプロイできているか確認してみましょう。
カスタムランタイムのLambdaが生まれていることがわかります!
また、DynamoDBテーブルも確認しましょう。
パーティションキー、ソートキーも正しく設定され、デプロイできていそうですね。
ここまで確認できればAPI GatewayのエンドポイントURLを取得して、APIにリクエストをしてみましょう。
$ http POST <endpoint> name=haw_ohnuma message="hello world from cargo lambda"
$ http <endpoint>
{
"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レコードが作成されていることを確認できます!
正しく動作していそうです!
ここまでできれば今回のcargo-lambda, cargo-lambda-cdkを使ったRustを使ったLambdaのAPI開発はおしまいです。ありがとうございました!
後片付け
マネコンのCloudFormationからStackの削除を実行することでまとめて消せます。
おわりに
cargo-lambda, cargo-lambda-cdkを用いたRustでのLambda開発の流れをハンズオン形式で紹介してみました。
誰かのお役に立てる記事となれば幸いです。
もっとこうしたほうがイイ!こここう書ける!みたいなことあったらコメントにて教えてください。