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エンドポイントにブラウザからアクセスしてみましょう。アクセスのたびにカウントが更新されます。
![カウンタがまわる様子](/static/a5ddeb63adb54286bacf52b0fe2dc1d7/a6d36/count.png)
アプリを削除する
アプリを削除するには cdk destroy
コマンドを利用します。DynamoDBのテーブルは以下の操作では削除されないので、別途削除しましょう。
cdk destroy
まとめ
RustでAWS Lambdaにデプロイするための一通りを試してみました。せっかくなので今つくっているアプリケーションの開発に役立てていこうと思います。ではでは〜
このカウンタは @piyoppi/counter-tools を使っています。
クリックすると匿名でいいねできます。