概要
この記事ではAWS SDK for Rustを使ってRDSにIAM認証で接続する方法を説明します。IAM認証のためにはRDSの認証トークンを生成する必要があるため、まずはその生成方法を説明します。次にRustのMySQLのクライアントライブラリであるmysqlクレートと、sqlxクレートのそれぞれ使ってRDSに接続する方法を説明します。
RDSのIAM認証
Amazon RDSではIAMを使ってユーザー認証を行うことができます。固定のパスワードを使わずに認証することができるため、セキュリティを向上させることができます。また、IAMによってデータベースへのアクセス権限を制御することができます。
このIAM認証を行うためには、IAM認証情報から認証トークンを生成してデータベースのパスワードとして使う必要があります。awscliを使って認証トークンを生成する場合は以下のコマンドを利用します。
aws rds generate-db-auth-token \
--hostname rdsmysql.123456789012.us-west-2.rds.amazonaws.com \
--port 3306 \
--region us-west-2 \
--username jane_doe
このトークンを使ってMySQLデータベースに接続するには、以下のようにいくつかのオプションを指定してmysqlコマンドを実行します。
mysql --host=hostName --port=portNumber --ssl-ca=full_path_to_ssl_certificate --enable-cleartext-plugin --user=userName --password=authToken
-
--ssl-ca
: RDSのSSL証明書を検証するためのCA証明書のパスを指定します。 -
--enable-cleartext-plugin
: パスワードとして指定した認証トークンをハッシュ化や暗号化せずに送信するためのオプションです。RDS側で認証トークンを処理するために必要です。
コマンドラインでは以上の手順で接続できます。以下ではRustプログラムから同様の手順でRDSに接続する方法を説明します。
RustでのRDS IAM認証トークンの生成
必要な依存関係の追加
まず必要なライブラリをCargo.tomlに追加します。以下が今回必要なライブラリを全て追加したCargo.tomlの例です。mysqlクレートとsqlxクレートは役割が重なるのでプロダクションコードではどちらか一方だけで十分です。
AWS SDKクレート群からはaws-config、aws-credential-types、aws-sigv4、aws-smithy-typesを利用します。
[package]
name = "connect-rds"
version = "0.1.0"
edition = "2021"
[dependencies]
aws-config = "1.1.1"
aws-credential-types = "1.1.1"
aws-sigv4 = "1.1.1"
aws-smithy-types = "1.1.1"
dotenvy = "0.15.7"
http = "1.0.0"
mysql = { version = "24.0.0", default-features = false, features = [
"default-rustls",
] }
sqlx = { version = "0.7.3", features = [
"runtime-tokio",
"tls-rustls",
"mysql",
] }
tokio = { version = "1", features = ["full"] }
環境変数の設定
次の準備として、.env
ファイルを作成して以下の環境変数を設定してください。dotenvyクレートを使ってこのファイルから読み込んだ値を環境変数に設定します。
AWSの認証情報は有効なIAMユーザーのアクセスキーとシークレットアクセスキーを設定します。MySQLのサーバー情報も環境に応じて適切なものを設定してください。
AWS_ACCESS_KEY_ID="..."
AWS_SECRET_ACCESS_KEY="..."
AWS_REGION="ap-northeast-1"
MYSQL_HOST="example.rds.amazonaws.com"
MYSQL_PORT="3306"
MYSQL_USER="root"
MYSQL_DATABASE="test"
認証トークンの生成コード
RDSの認証トークンを生成する関数の全体は以下のようになります。以下ではそれぞれの要素を詳しく見ていきましょう。
use aws_config::BehaviorVersion;
use aws_credential_types::provider::ProvideCredentials;
use aws_sigv4::http_request::{
sign, SignableBody, SignableRequest, SignatureLocation, SigningSettings,
};
use aws_sigv4::sign::v4;
use aws_smithy_types::body::SdkBody;
use http::Method;
use std::env;
use std::time::Duration;
use std::time::SystemTime;
pub type OpaqueError = Box<dyn std::error::Error + Send + Sync + 'static>;
pub async fn generate_rds_auth_token() -> Result<String, OpaqueError> {
let aws_config = aws_config::load_defaults(BehaviorVersion::latest()).await;
let identity = aws_config
.credentials_provider()
.ok_or("No Credentials Provider")?
.provide_credentials()
.await?
.into();
let mut signing_settings = SigningSettings::default();
signing_settings.signature_location = SignatureLocation::QueryParams;
signing_settings.expires_in = Some(Duration::from_secs(900));
let signing_params = v4::SigningParams::builder()
.identity(&identity)
.region(match aws_config.region() {
Some(region) => region.as_ref(),
None => "ap-northeast-1",
})
.name("rds-db")
.time(SystemTime::now())
.settings(signing_settings)
.build()?
.into();
let uri = format!(
"http://{}:{}/?Action=connect&DBUser={}",
env::var("MYSQL_HOST")?,
env::var("MYSQL_PORT")?.parse::<u16>()?,
env::var("MYSQL_USER")?
);
let signable_request = SignableRequest::new(
Method::GET.as_str(),
uri.clone(),
std::iter::empty(),
SignableBody::Bytes(&[]),
)?;
let mut request = http::Request::builder().uri(uri).body(SdkBody::empty())?;
let (signing_instructions, _signature) = sign(signable_request, &signing_params)?.into_parts();
signing_instructions.apply_to_request_http1x(&mut request);
let mut uri = request.uri().to_string();
let token = uri.split_off("http://".len());
Ok(token)
}
まずaws_configクレートを使ってAWSの認証情報を読み込んでSdkConfig
構造体を取得します。
let aws_config = aws_config::load_defaults(BehaviorVersion::latest()).await;
後続処理で利用するためにSdkConfig
からIdentity
構造体に変換しておきます。
let identity = aws_config
.credentials_provider()
.ok_or("No Credentials Provider")?
.provide_credentials()
.await?
.into();
次にaws_sigv4クレートで署名を作成する準備をしていきます。まずは署名の設定の構造体(SigningSettings
)を作成します。RDSの認証トークンを生成するためには署名の位置(signature_location
)をクエリパラメータに指定し、有効期限(expires_in
)を900秒に設定します。(900秒以外の値を指定すると接続エラーになります。)
let mut signing_settings = SigningSettings::default();
signing_settings.signature_location = SignatureLocation::QueryParams;
signing_settings.expires_in = Some(Duration::from_secs(900));
次にこのSigningSettings
と先ほど作成したIdentity
、リージョンやサービス名のパラメータを設定してSigningParams
構造体を作成します。
let signing_params = v4::SigningParams::builder()
.identity(&identity)
.region(match aws_config.region() {
Some(region) => region.as_ref(),
None => "ap-northeast-1",
})
.name("rds-db")
.time(SystemTime::now())
.settings(signing_settings)
.build()?
.into();
次に署名を行うHTTPリクエストを構成します。まずリクエストURIを作成します。RDSのホストとポートを環境変数から読み込み、またAction=connect
とDBUser={MySQLユーザー名}
をクエリパラメータに指定します。次にSignableRequest
構造体を作成します。メソッドはGET、URIは先ほど作成したURI、ヘッダーおよびリクエストボディは空白にします。
let uri = format!(
"http://{}:{}/?Action=connect&DBUser={}",
env::var("MYSQL_HOST")?,
env::var("MYSQL_PORT")?.parse::<u16>()?,
env::var("MYSQL_USER")?
);
let signable_request = SignableRequest::new(
Method::GET.as_str(),
uri.clone(),
std::iter::empty(),
SignableBody::Bytes(&[]),
)?;
このSignableRequest
と先ほど作成したSigningParams
を使って署名を行います。aws_sigv4::http_request::sign
関数から署名の指示と署名自体の2つの値が生成されます(into_parts()
でそれぞれを別の値にしたタプルにしています)。このうち署名の指示部分をHTTPリクエストに適用して認証トークンを作成します。
let (signing_instructions, _signature) = sign(signable_request, &signing_params)?.into_parts();
署名を適用するためのHTTPリクエストを作成します。
let mut request = http::Request::builder().uri(uri).body(SdkBody::empty())?;
このリクエストに署名の指示を適用します。これによりHTTPリクエストのURIのクエリパラメータに署名情報が追加されます。このURIからプロトコル部分を削除すると認証トークンになります。
signing_instructions.apply_to_request_http1x(&mut request);
let mut signed_uri = request.uri().to_string();
let token = signed_uri.split_off("http://".len());
以下のようなユニットテストでこの関数から出力される認証トークンを確認できます。実行前にdotenvyクレートを使って.env
ファイルから環境変数を読み込んでおく必要があります。
#[cfg(test)]
mod tests {
use super::*;
use dotenvy::dotenv;
#[tokio::test]
async fn test_generate_rds_auth_token() {
dotenv().ok();
let rds_auth_token = generate_rds_auth_token().await.unwrap();
println!("{}", rds_auth_token);
}
}
このユニットテストを実行すると、以下のような認証トークンが生成されたことが確認できます。
example.rds.amazonaws.com:3306/?Action=connect&DBUser=root&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...%2F20240202%2Fap-northeast-1%2Frds-db%2Faws4_request&X-Amz-Date=20240202T052602Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=...
RDSのSSL証明書の取得
次にこの認証トークンを使ってRDSに接続していきますが、その前にRDSのSSL証明書を検証するためのCA証明書を取得する必要があります。以下のリンクからglobal-bundle.pem(全リージョンの証明書バンドルファイル)またはap-northeast-1-bundle.pemなどのリージョンごとの証明書バンドルファイルをダウンロードしてください。
RDSへの接続
mysqlクレートの利用
mysqlクレートを使ってRDSに接続する関数は以下のようになります。OptsBuilder
を利用して接続オプションを設定し、Conn::new()
で接続を行います。
MySQLサーバーの各設定情報は環境変数から読み込みます。パスワードは先ほど作成した認証トークンを使います。コマンドラインでの例と同じようにenable_cleartext_plugin()
で平文でトークンを送るようにします。ssl_opts()
でSSL証明書をCA証明書で検証するよう指示して、そのCA証明書のパス指定します。このパスには前のセクションでダウンロードした証明書バンドルファイルのパスを指定してください。
use std::env;
use crate::OpaqueError;
pub fn get_mysql_connection(rds_auth_token: &String) -> Result<mysql::Conn, OpaqueError> {
let opts = mysql::OptsBuilder::new()
.ip_or_hostname(env::var("MYSQL_HOST").ok())
.tcp_port(env::var("MYSQL_PORT")?.parse::<u16>()?)
.user(env::var("MYSQL_USER").ok())
.pass(Some(rds_auth_token))
.db_name(env::var("MYSQL_DATABASE").ok())
.enable_cleartext_plugin(true)
.ssl_opts(mysql::SslOpts::default().with_root_cert_path(
std::path::Path::new("/path/to/certificate/file").into(),
));
let conn = mysql::Conn::new(opts)?;
Ok(conn)
}
sqlxクレートの利用
sqlxクレートを使ってRDSに接続する関数は以下のようになります。MySqlConnectOptions
を利用して接続オプションを設定し、connect()
で接続を行います。
mysqlクレートの場合と同様にパスワードは認証トークンを使います。こちらにもenable_cleartext_plugin()
オプションがあります。ssl_mode()
でVerifyCa
を指定してCA証明書によるSSL証明書の検証を行うよう指示します。またssl_ca()
でCA証明書のパスを指定します。こちらも前のセクションでダウンロードした証明書バンドルファイルのパスを指定してください。
use std::env;
use sqlx::mysql::{MySqlConnectOptions, MySqlConnection, MySqlSslMode};
use sqlx::ConnectOptions;
use crate::OpaqueError;
pub async fn get_mysql_connection_with_sqlx(
rds_auth_token: &String,
) -> Result<MySqlConnection, OpaqueError> {
let options = MySqlConnectOptions::new()
.host(&env::var("MYSQL_HOST")?)
.port(env::var("MYSQL_PORT")?.parse::<u16>()?)
.username(&env::var("MYSQL_USER")?)
.password(rds_auth_token)
.database(&env::var("MYSQL_DATABASE")?)
.ssl_mode(MySqlSslMode::VerifyCa)
.ssl_ca("/path/to/certificate/file")
.enable_cleartext_plugin(true);
let conn = options.connect().await?;
Ok(conn)
}
参考文献
認証トークンの生成方法について以下のissueを参考にしました。ただしSDKのバージョンが古いためこのissueで説明されているコードは現在使えません。