この記事はRust+SvelteKit+CDKでRSS要約アプリを作ってみる Advent Calendar 2025の11日目の記事になります。
また、筆者が属している株式会社野村総合研究所のアドベントカレンダーもあるので、ぜひ購読ください。
はじめに
今回は学習目的でRustを使ってLambdaを書いてみました。Node.jsやPythonで書かれたLambdaを見たことはあるのですがRustは見たことなかったので手探り状況でしたが、どうやったら動くのかを解説したいと思います。
RustでLambdaを書いた時のメリット(一般的な話)
学習目的でなくとも、RustでLambdaを書くメリットはあると思いました。Rustはネイティブバイナリにコンパイルされるため、Node.jsやPythonのようなランタイムオーバーヘッドがないのがその理由です。これにより、コールドスタート(初期起動)が非常に高速になります。また、実行時間が短くメモリ消費も少ないのでコストの点でも恩恵があります。
SumaRSSでの実装
SumaRSSでは、RSS収集(Collector)と要約(Processor)の2つのLambda関数をRustで実装しています。
プロジェクト構成
cargo-lambdaで生成される標準的な構成です。cargo-lambdaについては、別の記事で紹介しています。cargo lambdaというサブコマンドを実行できるようになります。
[package]
name = "lambda"
# ...
[dependencies]
lambda_runtime = "0.13.0"
tokio = { version = "1", features = ["macros"] }
tracing = "0.1"
aws_lambda_events = { version = "0.15.1", features = ["eventbridge", "sqs"] }
# ...
ハンドラの実装
ハンドラはcargo lambda newで作成したディレクトリのsrc/bin配下に定義します。一つのハンドラにつき一つの.rsファイルを作成します。今回はcargo lambda newで、cdlディレクトリの下にlambdaディレクトリを作成したので、cdk/lambda/src/bin/{任意のファイル名}/rsが一つのハンドラになります。
RSS収集を担うcdk/lambda/src/bin/collector.rsのmain関数の実装を紹介します。
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
use aws_lambda_events::eventbridge::EventBridgeEvent;
#[tokio::main]
async fn main() -> Result<(), Error> {
// ログ出力の初期化(CloudWatch Logsに出力される)
tracing::init_default_subscriber();
// ハンドラ関数を登録してLambdaランタイムを起動
run(service_fn(function_handler)).await
}
// ハンドラ関数
// EventBridgeからのイベントを受け取る例
async fn function_handler(_event: LambdaEvent<EventBridgeEvent>) -> Result<(), Error> {
info!("🚀 Collector function started");
// ここにビジネスロジックを書く
Ok(())
}
実装のポイント
1. lambda_runtimeクレート
AWSが提供する公式ランタイムライブラリです。run関数に、service_fnでラップした自作の非同期関数を渡すだけで動きます。ちなみに、このクレーとはcargo-lambdaでプロジェクトを作成すると自動的に追加されます。
2. 型安全なイベント定義
aws_lambda_eventsクレートを使うと、S3、SQS、EventBridgeなど、AWSサービスのイベントペイロードの型定義を利用できます。Lambdaの呼び出し方はケースバイケースだと思うので、ハンドラに応じたイベントの型を使って引数を定義しましょう。上記ではEventBridgeから呼び出されるときの型(EventBridgeEvent)を使用しています。
また、エラーもlambda_runtimeが提供するものを使いましょう。
3. 非同期ランタイム (Tokio)
Lambdaの実行環境は非同期処理が基本です。#[tokio::main]マクロを使って非同期ランタイムを初期化します。
4. 構造化ロギング (Tracing)
println!ではなくtracingクレートを使うことで、JSON形式の構造化ログを出力したり、ログレベル(INFO, ERROR, DEBUG)を制御したりできます。これを使うことでCloudWatchなどでのトレーサビリティが上がるハズです(まだがっつり運用とかはしていないので実感はしていないですが、これがベストプラクティスなはず・・・)。
ローカルでLambdaを実行する
cargo lambda invoke collector --data-file test-event.json
このコマンドを叩くと、ローカルでLambda関数が実行されます。このとき読み込ませるJSONファイルは、ハンドラが期待するイベント構造に従っている必要があります(筆者はAIで叩き台を作って、トライアンドエラーで修正しています)。
SQSからのイベントを処理するケース
SQSからのイベントを処理するパターンも載せておきます。SQS経由で呼び出すcdk/lambda/src/bin/processor.rsは、SQSメッセージをパースして順番に処理します。ハンドラのシグネチャを変えるだけで、SQSイベントを型安全に扱えます。
use aws_lambda_events::event::sqs::SqsEvent;
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
use serde::Deserialize;
use tracing::info;
#[derive(Deserialize)]
struct SqsMessage {
article_id: String,
s3_key: String,
}
async fn function_handler(event: LambdaEvent<SqsEvent>) -> Result<(), Error> {
info!("🚀 Processor function started");
for record in event.payload.records {
// bodyはOption<String>なので欠損に注意
let body = record.body.as_deref().unwrap_or_default();
let msg: SqsMessage = serde_json::from_str(body)?; // JSONを構造体にパース
info!("processing article_id: {}", msg.article_id);
// ここでS3から本文取得→要約→DynamoDBへ保存→SQS削除…といった処理を行う
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Error> {
run(service_fn(function_handler)).await
}
まとめ
cargo-lambdaが優秀すぎるので、特に詰まることなく実装できました。具体的にどのようなイベントの中身になっているのかはドキュメントやコードの中身を読まなければ最初は分かりにくいですが、そこまで何パターンも実装することもないと思うので、非常にコーディングのハードルは低いように感じました。