こんにちは😸
カスタマーサクセス部の山本です。
AWS Secrets Managerから認証情報を取得してデータベースに接続することにより、プログラム側でのDB認証情報のハードコーディングを排除することができます。dblinkの場合はどうなるのか、気になったのでやってみました。
概要
本記事では、Aurora PostgreSQL 環境で dblink 接続を使用する際の、データベースパスワードの安全な管理方法について解説します。AWS Secrets Managerを活用することで、パスワード管理における運用負荷の軽減とセキュリティの向上を実現できます。
特徴
以下の通りです:
- パスワードのハードコーディングを排除
- AWS Secrets Managerによる動的なパスワード取得
- SSL接続による通信の暗号化
構成
以下の4つのコンポーネントで構成しています:
- Aurora PostgreSQL: メインのデータベースエンジン
- AWS Lambda: パスワードを取得するための関数
- AWS Secrets Manager: パスワードを安全に保管
- リモートDB: dblink接続の接続先
処理の流れ
概略図:
処理の流れは以下のように実装しています:
シークレット取得プロセス
- Aurora PostgreSQLからLambda関数の呼び出し
- Lambda関数によるSecrets Managerへのシークレット要求
- Secrets Managerによる暗号化シークレットの復号化
- Lambda関数によるパスワード抽出とPostgreSQLへの返却
- 取得したパスワードによるdblink接続の確立
エラーハンドリング
- シークレット取得失敗時:Lambda関数からエラーメッセージを返却
- 接続エラー発生時:PostgreSQL関数から詳細情報を返却
セキュリティ対策
- すべての通信はSSL/TLSで暗号化
- パスワードはメモリ上でのみ扱います
- セッション終了時に自動的に接続を切断
セットアップ手順
1. IAMロールの設定
まず、Aurora PostgreSQL用のIAMロールを作成します:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "lambda:InvokeFunction", "Resource": "arn:aws:lambda:region:account:function:get-secret-function" } ] }
作成したロールをAurora PostgreSQLクラスターに関連付けます:
aws rds add-role-to-db-cluster \ --db-cluster-identifier your-cluster-identifier \ --role-arn arn:aws:iam::account:role/your-role-name \ --feature-name Lambda
--feature-name
パラメータについて:
Lambda
: Lambda関数を呼び出すための機能を有効化します- 機能の種類を指定します(S3なら
s3Export
) - aws_lambda拡張機能に必要な設定です
- クラスター全体で一度設定すればOK
マネジメントコンソールでの該当箇所です。
2. AWS Secrets Managerの設定
データベースの認証情報は、以下のようなJSON形式で保存します:
{ "username": "dbuser", // ユーザー名 "password": "your-password", // パスワード "host": "your-host", // ホスト名 "port": 5432, // ポート番号 "dbname": "your-database" // DB名 }
AWS CLIを使用したシークレットの作成例:
aws secretsmanager create-secret \ --name your-secret-name \ --secret-string '{"username":"dbuser","password":"your-password","host":"your-host","port":5432,"dbname":"your-database"}'
実装における注意点:
- シークレット名の一意性を確保
- パスワードの定期的なローテーション
- 最小権限の原則に基づくアクセス制御
マネジメントコンソールでの該当箇所です。
3. Lambda関数のデプロイ
Lambda関数(get_secret.py)の実装例:
import json import boto3 from botocore.exceptions import ClientError def get_secret(secret_name): """シークレットを取得する関数""" session = boto3.session.Session() client = session.client( service_name='secretsmanager', region_name='ap-northeast-1' # 東京リージョン ) try: get_secret_value_response = client.get_secret_value( SecretId=secret_name ) except ClientError as e: raise e else: if 'SecretString' in get_secret_value_response: secret = json.loads(get_secret_value_response['SecretString']) return secret.get('password', '') def lambda_handler(event, context): """Lambda関数のメイン処理""" try: secret_name = event['secret_name'] password = get_secret(secret_name) return password except Exception as e: return f"Error: {str(e)}"
Lambda関数には以下の権限が必要です:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret" ], "Resource": "arn:aws:secretsmanager:region:account:secret:secret-name" } ] }
Lambda関数の入出力例:
- 正常系
入力(payload.json):
{ "secret_name": "test-db-secret" }
出力:
"TestPassword123!"
- エラー系(シークレットが存在しない場合)
入力(payload.json):
{ "secret_name": "non-existent-secret" }
出力:
"Error: An error occurred (ResourceNotFoundException) when calling the GetSecretValue operation: Secrets Manager can't find the specified secret."
- エラー系(パラメータ不足)
入力(payload.json):
{}
出力:
"Error: 'secret_name' is required in the event"
4. PostgreSQL関数の作成
まず、必要な拡張機能をインストールします:
CREATE EXTENSION IF NOT EXISTS aws_lambda CASCADE; CREATE EXTENSION IF NOT EXISTS dblink;
シークレット取得用の関数を作成:
CREATE OR REPLACE FUNCTION get_secret_value( secret_name text ) RETURNS json LANGUAGE plpgsql AS $$ DECLARE lambda_response text; BEGIN -- Lambda関数を呼び出してシークレットを取得 SELECT aws_lambda.invoke( aws_commons.create_lambda_function_arn('get-secret-function', 'ap-northeast-1'), json_build_object('secret_name', secret_name) ) INTO lambda_response; -- パスワードを抽出して返却 RETURN json_build_object('password', regexp_replace(lambda_response, '^.*?"([^"]+)".*?$', '\1')); END; $$;
接続用の関数を作成:
CREATE OR REPLACE FUNCTION connect_with_secret( secret_name text, remote_host text, remote_db text, remote_user text, remote_port int DEFAULT 5432 ) RETURNS text LANGUAGE plpgsql AS $$ DECLARE connection_string text; db_password text; secret_json json; BEGIN -- シークレットからパスワードを取得 SELECT get_secret_value(secret_name) INTO secret_json; db_password := secret_json->>'password'; -- SSL必須の接続文字列を構築 connection_string := format( 'host=%s port=%s dbname=%s user=%s password=%s sslmode=require', remote_host, remote_port, remote_db, remote_user, db_password ); -- 既存の接続を安全に切断 BEGIN PERFORM dblink_disconnect('remote_conn'); EXCEPTION WHEN OTHERS THEN NULL; END; -- 新しい接続を確立 PERFORM dblink_connect('remote_conn', connection_string); RETURN 'Connection established successfully'; EXCEPTION WHEN OTHERS THEN RETURN 'Connection failed: ' || SQLERRM; END; $$;
使用方法
基本的な使用例
- シークレット取得の動作確認:
SELECT get_secret_value('test-db-secret');
実行結果:
get_secret_value ----------------------------------- {"password" : "TestPassword123!"} (1 row)
- dblink接続を確立:
SELECT connect_with_secret( 'test-db-secret', 'aurora-demo-cluster.cluster-xxx.ap-northeast-1.rds.amazonaws.com', 'testdb', 'postgres' );
実行結果:
connect_with_secret ------------------------------------- Connection established successfully (1 row)
- リモートクエリを実行:
SELECT * FROM dblink( 'remote_conn', 'SELECT tablename FROM pg_tables WHERE schemaname = ''public''' ) AS t(tablename text);
実行結果:
tablename ----------- users products orders (3 rows)
- 接続を切断:
SELECT dblink_disconnect('remote_conn');
実行結果(接続が既に切断されている場合):
ERROR: connection "remote_conn" not available
注意事項:PostgreSQLのセッション終了時にdblink接続は自動的に切断されます。
エラーケースと対処法
- シークレット名が間違っている場合:
ERROR: Error: Secrets Manager Error: Secrets Manager can't find the specified secret.
- IAMロールの設定ミス:
ERROR: Error: Lambda invocation failed: User is not authorized to perform lambda:InvokeFunction
- 接続パラメータが不正:
connect_with_secret ----------------------------------------------------- Connection failed: could not connect to server: Connection refused (1 row)
制限事項
- Aurora PostgreSQL であれば、aws_lambda と dblink の拡張機能が使えるので、対応できそうです
- PostgreSQL関数でパスワードを抽出際に使用している正規表現の制約上、パスワードに
"
を含めることができません
参考情報
余談
近所の梅が綺麗でした。