この記事はRust+SvelteKit+CDKでRSS要約アプリを作ってみる Advent Calendar 2025の13日目の記事になります。
また、筆者が属している株式会社野村総合研究所のアドベントカレンダーもあるので、ぜひ購読ください。
はじめに
RustからDynamoDBを操作する場合、公式のAWS SDK for Rust (aws-sdk-dynamodb) が便利です。しかし、構造体とDynamoDBのAttributeValue(型付きJSONのようなもの)との変換までは実装されていません。そこで今回は、serde_dynamoというクレートを組み合わせて、Rustの構造体をそのままDynamoDBに保存・取得する方法を採用しました。
AWS SDK for Rustとserde_dynamo
AWS SDK for Rust
AWS公式が提供するSDKで、非同期処理(Tokio)を前提に設計されています。
aws_config::load_defaultsで設定を読み込み、aws_sdk_dynamodb::Clientを作成して操作します。
AttributeValueの壁
DynamoDBのアイテムはHashMap<String, AttributeValue>という形式で表現されます。
例えば、{"id": "123", "count": 1}というデータを保存するには、以下のような記述になります。
// SDKをそのまま使うと大変...
let item = HashMap::from([
("id".to_string(), AttributeValue::S("123".to_string())),
("count".to_string(), AttributeValue::N("1".to_string())),
]);
serde_dynamoによる解決
serde_dynamoを使うと、Serialize/Deserializeを実装したRustの構造体を、自動的にこのHashMap<String, AttributeValue>に変換してくれます。これにより、ビジネスロジックで扱う型とDB操作をシームレスに繋ぐことができます。
そもそもRustにはserdeという非常に便利なSerialize/Deserializeを扱えるクレートがあります。
SumaRSSでの実装
cdk/lambda/src/lib.rsのDynamDbRepositoryクラスの実装を見てみます。
依存関係
[dependencies]
aws-config = "1"
aws-sdk-dynamodb = "1"
serde = { version = "1", features = ["derive"] }
serde_dynamo = { version = "4", features = ["aws-sdk-dynamodb+1"] }
実装コード(保存処理)
pub async fn put_item<T>(&self, item: &T) -> Result<(), Error>
where
T: Serialize,
{
// 1. 構造体をDynamoDB形式(HashMap)に変換
let item_map = serde_dynamo::to_item(item).unwrap();
// 2. SDKを使って保存
self.client
.put_item()
.table_name(&self.table_name)
.set_item(Some(item_map))
.send()
.await?;
Ok(())
}
実装コード(取得処理)
pub async fn list_items<T>(&self) -> Result<Vec<T>, DynamDbListItemsError>
where
T: DeserializeOwned,
{
// 1. Scan実行
let results = self.client
.scan()
.table_name(&self.table_name)
.send()
.await?;
// 2. 結果を構造体のベクタに変換
let items = results
.items()
.iter()
.map(|item| {
// AttributeValueのMapから構造体へ変換
serde_dynamo::from_item(item.clone())
})
.collect::<Result<Vec<T>, _>>()?;
Ok(items)
}
実装のポイント
保存時はto_itemを、取得時はfrom_itemを使うことで、DynamoDBのアイテムと構造体とを相互に変換可能です(実装例では無邪気に.unwrapを使っていますが本当はもっと丁寧にしたほうがいい気がします)。
put_item<T>やlist_items<T>のようにジェネリクスを使うことで、Article構造体でもFeed構造体でも、同じリポジトリクラスで扱えるようにしています。条件としてSerializeやDeserializeOwnedトレイトを実装していることを要求することで型安全にしています。
SDKの初期化
SDKの初期化部分ではまずconfigを組み立てて、それをベースにクライアントを作成します。configの組み立て時にロジックを組むことで、ローカルで実行する時とLambdaで実行するときとで柔軟に切り替えることができます。
let config = get_sdk_config().await;
let dynamodb_config = if let Ok(endpoint_url) = std::env::var("AWS_ENDPOINT_URL") {
debug!(endpoint_url = %endpoint_url, "Using custom DynamoDB endpoint");
aws_sdk_dynamodb::config::Builder::from(&config)
.endpoint_url(endpoint_url)
.build()
} else {
aws_sdk_dynamodb::config::Builder::from(&config).build()
};
let client = aws_sdk_dynamodb::Client::from_conf(dynamodb_config);
まとめ
aws-sdk-dynamodbとserde_dynamoを組み合わせることで、Rustの型安全性を維持したまま、直感的にDynamoDBを操作できました。