api_handler/db.rs
1//! # データベース接続モジュール
2//!
3//! このモジュールは、Amazon Aurora DSQL への接続管理を担当します。
4//! [`aurora_dsql_sqlx_connector`] クレートを使用して IAM 認証付きの接続プールを構築し、
5//! [`sea_orm`] の [`DatabaseConnection`] として提供します。
6//!
7//! ## Aurora DSQL について
8//!
9//! Amazon Aurora DSQL は、IAM ロールベースの認証を使用するサーバーレス分散 SQL データベースです。
10//! 通常の PostgreSQL と異なり、ユーザー名はロール名、パスワードは IAM 認証トークンで自動生成されます。
11//! このモジュールでは [`aurora_dsql_sqlx_connector`] がトークン生成と更新を自動的に処理します。
12//!
13//! ## 接続文字列の形式
14//!
15//! ```text
16//! postgres://<role>@<endpoint>/postgres?region=<region>
17//! ```
18//!
19//! - `<role>`: データベースロール名(例: `crudrole`, `selectview`)
20//! - `<endpoint>`: Aurora DSQL クラスターのエンドポイント(例: `abc123.dsql.ap-northeast-1.on.aws`)
21//! - `<region>`: AWSリージョン(例: `ap-northeast-3`)
22//!
23//! ## 使用するロール
24//!
25//! Lambda 関数では以下のロールを使用します:
26//! - `crudrole`: お問い合わせの作成・取得に使用(CRUD権限)
27
28use aurora_dsql_sqlx_connector::pool;
29use lambda_runtime::Error;
30use sea_orm::{DatabaseConnection, SqlxPostgresConnector};
31
32/// Aurora DSQL への接続文字列を構築する
33///
34/// 指定されたロール名、エンドポイント、リージョンから PostgreSQL 形式の接続文字列を生成します。
35/// [`aurora_dsql_sqlx_connector`] がこの文字列を解析して IAM 認証トークンを取得し、
36/// 実際のデータベース接続を確立します。
37///
38/// # Arguments
39///
40/// * `role` - データベースロール名(例: `"crudrole"`, `"selectview"`)
41/// * `endpoint` - Aurora DSQL クラスターのエンドポイントホスト名
42/// (例: `"abc123.dsql.ap-northeast-1.on.aws"`)
43/// * `region` - Aurora DSQL クラスターが存在する AWS リージョン
44/// (例: `"ap-northeast-3"`)
45///
46/// # Returns
47///
48/// PostgreSQL URI 形式の接続文字列。
49/// `aurora_dsql_sqlx_connector` に渡すことで、IAM 認証が自動的に処理されます。
50///
51/// # Examples
52///
53/// ```
54/// let conn_str = build_connection_string(
55/// "crudrole",
56/// "abc123.dsql.ap-northeast-1.on.aws",
57/// "ap-northeast-1"
58/// );
59/// assert_eq!(
60/// conn_str,
61/// "postgres://crudrole@abc123.dsql.ap-northeast-1.on.aws/postgres?region=ap-northeast-1"
62/// );
63/// ```
64fn build_connection_string(role: &str, endpoint: &str, region: &str) -> String {
65 format!("postgres://{role}@{endpoint}/postgres?region={region}")
66}
67
68/// Aurora DSQL への SeaORM データベース接続を作成する
69///
70/// 指定されたロール・エンドポイント・リージョンを使用して Aurora DSQL への接続プールを構築し、
71/// [`sea_orm::DatabaseConnection`] として返します。
72///
73/// 接続確立には IAM 認証が使用されます。[`aurora_dsql_sqlx_connector`] が AWS STS と通信して
74/// 認証トークンを自動取得します。そのため、Lambda 関数の実行ロールに
75/// `dsql:DbConnectAdmin` または `dsql:DbConnect` 権限が必要です。
76///
77/// # Arguments
78///
79/// * `role` - Aurora DSQL のデータベースロール名(例: `"crudrole"`)
80/// * `endpoint` - Aurora DSQL クラスターのエンドポイントホスト名
81/// * `region` - Aurora DSQL クラスターが存在する AWS リージョン
82///
83/// # Returns
84///
85/// * `Ok(DatabaseConnection)` - 正常に接続が確立された場合
86/// * `Err(Error)` - 接続に失敗した場合(IAM権限不足、ネットワークエラー等)
87///
88/// # Errors
89///
90/// - `aurora_dsql_sqlx_connector::pool::connect` が失敗した場合(IAM認証エラー、接続拒否等)
91/// `"Failed to connect to database: ..."` メッセージを含む [`anyhow::Error`] を返します。
92///
93/// # Panics
94///
95/// この関数はパニックしません。
96pub(crate) async fn create_db(role: &str, endpoint: &str, region: &str) -> Result<DatabaseConnection, Error> {
97 tracing::info!("Creating database connection with Aurora DSQL SQLx connector...");
98 let connection_string = build_connection_string(role, endpoint, region);
99 let pool = pool::connect(&connection_string)
100 .await
101 .map_err(|e| anyhow::anyhow!("Failed to connect to database: {}", e))?;
102
103 Ok(SqlxPostgresConnector::from_sqlx_postgres_pool(pool))
104}
105
106#[cfg(test)]
107mod tests {
108 use super::build_connection_string;
109
110 #[test]
111 fn test_build_connection_string() {
112 let endpoint = "foo0bar1baz2quux3quuux4.dsql.ap-northeast-1.on.aws";
113 let region = "ap-northeast-1";
114 let role = "selectview";
115
116 let result = build_connection_string(role, endpoint, region);
117
118 assert_eq!(
119 result,
120 "postgres://selectview@foo0bar1baz2quux3quuux4.dsql.ap-northeast-1.on.aws/postgres?region=ap-northeast-1"
121 );
122 }
123}
124
125#[cfg(test)]
126mod prop_tests {
127 use super::build_connection_string;
128 use proptest::prelude::*;
129
130 proptest! {
131 /// 任意のエンドポイントとリージョンで接続文字列が正しい形式になる
132 #[test]
133 fn prop_connection_string_contains_endpoint_and_region(
134 endpoint in "[a-z0-9][a-z0-9\\-]{0,30}\\.[a-z0-9\\.]{1,30}",
135 region in "[a-z]{2,6}-[a-z]{4,10}-[0-9]",
136 ) {
137 let role = "selectview";
138 let result = build_connection_string(role, &endpoint, ®ion);
139 let role_prefix = format!("postgres://{role}@");
140 prop_assert!(result.starts_with(&role_prefix));
141 prop_assert!(result.contains(&endpoint));
142 let region_param = format!("region={}", region);
143 prop_assert!(result.contains(®ion_param));
144 prop_assert!(result.contains("/postgres?"));
145 }
146
147 /// 接続文字列は常に postgres:// スキームで始まる
148 #[test]
149 fn prop_connection_string_scheme(
150 endpoint in "[a-z0-9\\.]{5,40}",
151 region in "[a-z0-9\\-]{5,20}",
152 ) {
153 let role = "selectview";
154 let result = build_connection_string(role, &endpoint, ®ion);
155 let role_prefix = format!("postgres://{role}@");
156 prop_assert!(result.starts_with(&role_prefix));
157 }
158 }
159}