![[Rust] Toasty を Amazon Aurora DSQL で動かす [DSQL]](https://images.ctfassets.net/ct0aopd36mqt/4u1gqKpsvlSCxsWDlEyXiJ/36c9da357c4278091af22c281a79ddca/rust-eyecatch.png?w=3840&fm=webp)
[Rust] Toasty を Amazon Aurora DSQL で動かす [DSQL]
Introduction
Toasty は Tokio チームが開発している Rust 製の ORM です。
現在SQLite / PostgreSQL / MySQL / DynamoDB をサポートしています。
先日 0.6系がリリースされました。
また、Amazon Aurora DSQL は PostgreSQL互換ですが、
外部キーや sequence が使えない・インデックス作成が非同期など多少の制約があるサーバレス分散DBです。
本記事では PostgreSQL対応してるからDSQLで動くかな?と思ったので試してみました。
DSQL?
Amazon Aurora DSQLはサーバレスの分散SQL データベースです。
特徴は以下。
- PostgreSQL 16 互換。psql や既存ドライバ(tokio-postgres 等)がそのまま使える
- マルチリージョン active-active / 自動スケール / 楽観的並行制御 (OCC)
- 接続は IAM。短命トークンをパスワードとして渡し、TLS 必須
- PostgreSQL の一部機能が非対応。
- 外部キー (FK)・sequence/SERIAL・トリガーなし
CREATE INDEXは同期版が不可で、CREATE INDEX ASYNCが必須- 1 トランザクションに DDL は 1 文まで(DDL と DML の混在も不可)
toastyとは
安心のTokio製async ORM。
derive マクロでモデルを定義すると、CRUD メソッドやスキーマ生成ができます。
#[derive(Debug, toasty::Model)]
struct User {
#[key]
#[auto] // 主キーを自動採番
id: uuid::Uuid,
name: String,
#[unique] // 一意制約 → get_by_email が使える
email: String,
}
使い方は以下。
push_schema() がモデル定義からテーブルを自動生成してくれます。
let mut db = toasty::Db::builder()
.models(toasty::models!(crate::*))
.connect("sqlite::memory:") // or postgresql://...
.await?;
// CREATE TABLE / INDEX を自動発行
db.push_schema().await?;
let user = toasty::create!(User { name: "shuta", email: "nakamura.shuta@classmethod.jp" })
.exec(&mut db).await?;
let got = User::get_by_id(&mut db, &user.id).await?;
接続文字列のschemeで使うドライバが決まります。
Environment
- Toasty:
v0.6.1(features =postgresql) - Rust: 1.95.0
- cargo-lambda: 1.9.1(arm64 ビルドは zig 0.16)
Setup
1. DSQL クラスター作成
まずはDSQLの使用準備。
% aws dsql create-cluster --region us-east-1 --no-deletion-protection-enabled
# → identifier と endpoint (<CLUSTER_ID>.dsql.us-east-1.on.aws) を取得
% aws dsql wait cluster-active --identifier "$CLUSTER_ID" --region us-east-1
2. 接続(IAM トークン + TLS)
DSQL はパスワードに IAM トークンを使います。
※トークンには & = / が含まれるため、URL に埋めるときは percent-encode 必須
sslmode=require も付けましょう。
% TOKEN=$(aws dsql generate-db-connect-admin-auth-token \
--hostname "$ENDPOINT" --region us-east-1 --expires-in 3600)
# postgresql://admin:<percent-encode した $TOKEN>@$ENDPOINT:5432/postgres?sslmode=require
3. Toasty プロジェクト作成
cargo newでプロジェクト作成します。
依存ライブラリは以下のようにします。
[dependencies]
toasty = { version = "0.6.1", features = ["postgresql"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
uuid = { version = "1", features = ["v4"] }
接続先は環境変数 TOASTY_CONNECTION_URL で
切り替えられるようにしておくと楽です。
Try
シンプルなモデルの基本動作
id + name だけの単一モデルが、push_schema() で
テーブル作成 → INSERT → 主キー取得まで動くか確認。
SQLite・ローカル PostgreSQL 16・DSQL のいずれも問題なく動作。
==> connected OK
==> push_schema OK (table created from model)
==> insert OK id=019e6281-9b66-7131-a529-5187af6daadf
==> get_by_id OK: name=shuta
push_schema() が生成する DDL はFK も sequence も無し。
DSQL の「FK 非対応・sequence 非対応」に元から当たらないため、secondary index が無ければそのまま動きます。
#[unique] / #[index] を付けると push_schema でエラー
一意制約やインデックスを持つモデルで push_schema() がどうなるか確認しました。
結果は#[unique] email を足すと、push_schema() が以下のようにエラー。
==> connected OK
Error: db error: ERROR: unsupported mode. please use CREATE INDEX ASYNC.
Toasty は CREATE UNIQUE INDEX ... を発行しますが、DSQL は同期的な CREATE INDEX を許可せず
CREATE INDEX ASYNC を要求します。
※CREATE TABLE 自体は成功しており、エラーはindex作成だけ
なお、Toasty 側に ASYNC を出させる設定は現状ありません。
なので、手動でCREATE INDEX ASYNCを実行し、push_schema は呼ばないようにしました。
DDL を Toasty に任せず、psql で手動作成します。
CREATE INDEX ASYNC は job_id を返すだけで、その時点では未完了です。
後続の DDL/DML の前に sys.wait_for_job(job_id) で完了を待ちます。
なお DSQL は「1 トランザクションに DDL 1 文まで」なので、
下の文は1 文ずつ別トランザクションで実行します。
CREATE TABLE users (id uuid NOT NULL, name text NOT NULL, email text NOT NULL, PRIMARY KEY (id));
-- 非同期インデックス作成
CREATE UNIQUE INDEX ASYNC index_users_by_email ON users (email);
-- job_id
-- ----------------------------
-- xxxxxxxxxxxxxxxxxxxxxxxxx
-- 完了を待ってから先へ
SELECT sys.wait_for_job('xxxxxxxxxxxxxxxxxxxxxxxxx');
アプリ側は #[unique] 付きのモデルをそのまま定義し、db.push_schema() を呼ばないにようにする。
その結果、#[unique] 付きモデルでもCRUD処理は問題なく動作しました。
==> skip push_schema (schema は手動作成: CREATE INDEX ASYNC)
==> insert OK id=019e628e-06ce-71b3-816a-53a0dc88a50e
==> get_by_id OK: name=Shuta
==> get_by_email OK: id=019e628e-06ce-71b3-816a-53a0dc88a50e
==> update OK: name=Shuta Updated
==> delete OK: get_by_id is_err=true
DDL は DSQL 流に自分で実行(index は ASYNC)し、
Toasty は接続とクエリに使うのが現状の動作方法です。
※今後ToastyがCREATE INDEX ASYNCを実行するようになれば解決と思われる
リレーション(#[has_many] / #[belongs_to])の確認
リレーションの作成・ロードが DSQL で動くかも確認してみます。
User ⇄ Todo を定義(Todo 側に #[index] user_id と #[belongs_to])。
Toasty は リレーションに FK を張らず user_id に index を作るだけなので、
その index を ASYNC で手動作成すれば動きます。
==> create todo (in user.todos)
==> create user2 + 2 todos
==> has_many load: user.todos()=1, user2.todos()=2
==> belongs_to load: todo.user().name=John Doe
nested create / バッチ nested create / has_many・belongs_to ロードが
すべて DSQLで成功しました。
AWS Lambda(cargo lambda)から DSQL に接続
ついでに Lambda 上でも動くか確認しました。
ここでは実行ロールの一時クレデンシャルから aws-sdk-dsql で接続トークンを生成します。
コードは「トークン生成して接続 → insert → 読み返して返す」だけのシンプルなもの
※テーブルは事前に手動作成
Setup の依存に加えて、Lambda 用に以下を追加します。
lambda_runtime = "1"
aws-config = { version = "1", features = ["behavior-version-latest"] }
aws-sdk-dsql = "1"
urlencoding = "2"
serde_json = "1"
use aws_config::{BehaviorVersion, Region};
use aws_sdk_dsql::auth_token::{AuthTokenGenerator, Config as DsqlAuthConfig};
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
use serde_json::{json, Value};
#[derive(Debug, toasty::Model)]
struct LambdaUser {
#[key] #[auto] id: uuid::Uuid,
note: String,
}
// 実行ロールのクレデンシャルから DSQL トークンを生成して接続
async fn connect() -> Result<toasty::Db, Error> {
let endpoint = std::env::var("DSQL_ENDPOINT")?;
let cfg = aws_config::load_defaults(BehaviorVersion::latest()).await;
let token = AuthTokenGenerator::new(
DsqlAuthConfig::builder()
.hostname(&endpoint).region(Region::new("us-east-1")).build()?,
)
.db_connect_admin_auth_token(&cfg).await?;
let url = format!(
"postgresql://admin:{}@{endpoint}:5432/postgres?sslmode=require",
urlencoding::encode(token.as_str()),
);
Ok(toasty::Db::builder().models(toasty::models!(crate::*)).connect(&url).await?)
}
async fn handler(_e: LambdaEvent<Value>) -> Result<Value, Error> {
let mut db = connect().await?;
let u = toasty::create!(LambdaUser { note: "hello from lambda" })
.exec(&mut db).await?;
let got = LambdaUser::get_by_id(&mut db, &u.id).await?;
Ok(json!({ "id": got.id.to_string(), "note": got.note }))
}
#[tokio::main]
async fn main() -> Result<(), Error> {
run(service_fn(handler)).await
}
実行ロールには dsql:DbConnectAdmin(対象クラスター ARN)を付与。
*DSQL endpoint はパブリックなのでVPC不要
ビルドとデプロイして確認してみましょう。
% cargo lambda build --release --arm64
% cargo lambda deploy --iam-role <ROLE_ARN> --env-var DSQL_ENDPOINT=$ENDPOINT --timeout 30
% cargo lambda invoke --remote toasty-dsql-lambda --data-ascii '{}'
問題なく動いてることが確認できました。
{"id":"019e62b4-4e4c-7090-86b5-e31f09976e30","note":"hello from lambda ..."}
Summary
本記事ではToasty(v0.6.1) を Aurora DSQLで試してみました。
結果的には基本的な CRUD・リレーション・Lambda からの接続は動作しました。
※ #[unique] / #[index] を付けると push_schema() がエラーになりますが
接続は TLS + IAM トークンでOK.(TLS 対応は issue #660 / PR #663 で導入済み)
secondary indexは問題があり、#[unique] / #[index] を付けると
Toasty が素の CREATE INDEX を発行し、DSQL の「CREATE INDEX ASYNC 必須」でエラーとなり、
push_schema() が失敗します。Toasty 側に ASYNC を出させる設定は今のところ無いので、
index だけ自分で CREATE INDEX ASYNC を実行して、push_schema() は呼ばないようにする必要があります。
シンプルなCRUD なら Toasty は DSQL で使えるが、
スキーマ(index)は DSQL 流に自分で用意すればとりあえず問題なさそうです。
※DSQL は CDC 同様まだ preview なので挙動が変わる可能性があるので注意
References
- GitHub - tokio-rs/toasty
- Toasty 0.6.0 - What is new? | Tokio
- toasty - docs.rs (0.6.1)
- toasty issue #660: toasty-driver-postgresql: Missing TLS support / PR #663
- [Rust] tokio の ORM「toasty」を今のうちに少しだけ (zenn)
- Amazon Aurora DSQL
- Asynchronous indexes in Aurora DSQL(
CREATE INDEX ASYNC/sys.wait_for_job/sys.jobs) - aws-sdk-dsql
AuthTokenGenerator(docs.rs) - cargo-lambda







