AWS CDKで作るサーバーレスデータベース基盤 【第3回】RDS Data API構築
こんにちは!製造ビジネステクノロジー部の小林です。
前回の記事ではAPI Gateway × Lambda × RDS Proxy の構築方法をご紹介しました。
今回は「AWS CDKで作るサーバーレスデータベース基盤」シリーズ第3回として、RDS Data APIを利用したデータベースアクセスについて解説します。
はじめに
本記事では、AWS CDKを使用してRDS Data APIを活用する方法を解説します。
解説する内容は以下になります。
- RDS Data APIの基本概念
- AWS CDKを使ったRDS Data API対応のインフラ構築
- Lambda関数からData APIを使用してデータベースにアクセスする方法
RDS Data APIとは
RDS Data APIは、データベースへの接続やコネクションプールを管理することなく、AWS SDKやHTTPリクエストを介してデータベース操作を可能にするサービスです。
主な特徴
- セキュアな HTTP エンドポイントを通じて SQLクエリを実行
- データベース接続の確立・管理が不要
- AWS SDKとの統合により様々な言語からアクセス可能
- AWS Secrets Managerに保存された認証情報を使用するため、API 呼び出し時に認証情報を渡す必要なし
- Lambda などのサーバーレスサービスからVPC設定なしでデータベースにアクセス可能
- クエリエディタでのアドホッククエリ実行をサポート
RDS Data APIの制限事項
- Aurora でのみ利用可能
- DB クラスターのライターインスタンスでのみ実行できる
- 読み取りクエリであっても、クエリ処理のためにライターインスタンスにアクセスする必要がある
- セカンダリクラスターに送信した読み取りクエリと書き込みクエリは、ライターインスタンスがない間は失敗する
- Tシリーズのインスタンスクラスはサポートしていない
RDS Proxy と DataAPIの違いは何か?
RDS Proxy はデータベースへの接続をプールすることでバックエンドのRDSの負荷を軽減しつつ、コネクション作成のコストを軽減します。Data API はデータベースのコネクションではなくAPIを利用してクエリを実行できる機能です。
公式ではこのように説明がありました。
前提条件
このハンズオンでは、前回の記事で構築した API Gateway と Lambda を利用します。
- ハンズオンを実際に試したい方: 前回の記事に沿って環境を構築してください。
- 概念や実装方法だけを知りたい方: 環境構築なしでも本記事の内容は理解できます。
システム構成図
以下は本シリーズで構築するシステムの全体像です。
今回の記事では、以下のリソースを構築します。
- Lambda(Data API接続用)
- Aurora Serverless v2(Data API有効化)
やってみる
※冗長なソースは折りたたんで表示しています。
環境
下記環境で実装しています。CDKはTypeScriptで実装しています。
$ sw_vers
ProductName: macOS
ProductVersion: 14.7.4
BuildVersion: 23H420
$ node -v
v20.18.3
$ npm -v
10.8.2
$ cdk --version
2.1003.0 (build b242c23)
必要なパッケージをインストール
# RDS Data Service のクライアントライブラリとSecrets Manager のクライアントライブラリをインストール
npm install @aws-sdk/client-rds-data @aws-sdk/client-secrets-manager
Aurora クラスターで Data API を有効にする
lib/aurora.tsファイルを修正。
aurora.ts
import * as cdk from 'aws-cdk-lib';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import { Construct } from 'constructs';
export class Aurora extends Construct {
public readonly cluster: rds.DatabaseCluster;
public readonly dbSecret: secretsmanager.Secret;
public readonly rdsProxy: rds.DatabaseProxy;
public readonly securityGroup: ec2.SecurityGroup;
constructor(scope: Construct, id: string, vpc: ec2.Vpc, lambdaSg?: ec2.SecurityGroup) {
super(scope, id);
// データベース認証情報のシークレットを作成
this.dbSecret = new secretsmanager.Secret(this, 'AuroraSecret', {
generateSecretString: {
secretStringTemplate: JSON.stringify({ username: 'postgres' }),
generateStringKey: 'password',
excludeCharacters: '/@" ',
},
});
// セキュリティグループの作成
const dbSecurityGroup = new ec2.SecurityGroup(this, 'DBSecurityGroup', {
vpc,
description: 'Security group for Aurora Serverless v2',
allowAllOutbound: true,
});
// VPC内からの接続を許可
dbSecurityGroup.addIngressRule(
ec2.Peer.ipv4(vpc.vpcCidrBlock),
ec2.Port.tcp(5432),
'Allow database connections from within VPC'
);
// Lambda からの接続を許可
if (lambdaSg) {
dbSecurityGroup.addIngressRule(
lambdaSg,
ec2.Port.tcp(5432),
'Allow connections from Lambda to RDS Proxy'
);
}
// Aurora Serverless v2クラスターの作成
this.cluster = new rds.DatabaseCluster(this, 'AuroraCluster', {
engine: rds.DatabaseClusterEngine.auroraPostgres({
version: rds.AuroraPostgresEngineVersion.VER_16_1,
}),
credentials: rds.Credentials.fromSecret(this.dbSecret),
defaultDatabaseName: 'demodb',
vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
securityGroups: [dbSecurityGroup],
writer: rds.ClusterInstance.serverlessV2('Writer', {
autoMinorVersionUpgrade: true,
}),
serverlessV2MinCapacity: 0.5,
serverlessV2MaxCapacity: 1,
enableDataApi: true, // Data API を有効化
});
// RDS Proxyの作成
this.rdsProxy = new rds.DatabaseProxy(this, 'AuroraProxy', {
proxyTarget: rds.ProxyTarget.fromCluster(this.cluster),
secrets: [this.dbSecret],
vpc,
securityGroups: [dbSecurityGroup],
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
requireTLS: false,
idleClientTimeout: cdk.Duration.seconds(900),
dbProxyName: 'aurora-serverless-proxy',
debugLogging: true,
});
}
}
Lambdaクラスを修正してData API Lambdaを追加
lib/lambda.ts ファイルを修正。
lambda.ts
import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';
import * as path from 'path';
import * as cdk from 'aws-cdk-lib';
import * as lambdaBase from 'aws-cdk-lib/aws-lambda';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as iam from 'aws-cdk-lib/aws-iam'; // 追加
export class Lambda extends Construct {
public readonly rdsProxyLambda: lambda.NodejsFunction;
public readonly securityGroup: ec2.SecurityGroup;
public readonly dataApiLambda: lambda.NodejsFunction; // 追加
constructor(scope: Construct,
id: string,
vpc: ec2.Vpc,
dbSecret: secretsmanager.ISecret,
rdsProxy: rds.DatabaseProxy,
cluster: rds.DatabaseCluster // 追加
) {
super(scope, id);
// Lambda用セキュリティグループ
this.securityGroup = new ec2.SecurityGroup(this, 'LambdaSG', {
vpc,
description: 'Security group for Lambda function',
allowAllOutbound: true,
});
// RDS Proxy を使用する Lambda
this.rdsProxyLambda = new lambda.NodejsFunction(this, 'RdsProxyLambda', {
entry: path.resolve(__dirname, '../lambda/rds-proxy-lambda.ts'),
handler: 'handler',
runtime: lambdaBase.Runtime.NODEJS_20_X,
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
securityGroups: [this.securityGroup],
timeout: cdk.Duration.seconds(30),
memorySize: 128,
environment: {
PROXY_ENDPOINT: rdsProxy.endpoint,
SECRET_ARN: dbSecret.secretArn,
DB_NAME: 'demodb',
},
bundling: {
forceDockerBundling: false, // Dockerを使用しない
minify: true,
nodeModules: ['pg', 'aws-sdk'],
},
});
// Data API を使用する Lambda (VPC外で実行可能) (追加)
this.dataApiLambda = new lambda.NodejsFunction(this, 'DataApiLambda', {
entry: path.resolve(__dirname, '../lambda/data-api-lambda.ts'),
handler: 'handler',
runtime: lambdaBase.Runtime.NODEJS_20_X,
timeout: cdk.Duration.seconds(30),
memorySize: 128,
environment: {
CLUSTER_ARN: cluster.clusterArn,
SECRET_ARN: dbSecret.secretArn,
DB_NAME: 'demodb',
},
bundling: {
forceDockerBundling: false,
minify: true,
nodeModules: ['@aws-sdk/client-rds-data', '@aws-sdk/client-secrets-manager'],
},
});
// Secrets Managerへのアクセス権限を付与
dbSecret.grantRead(this.rdsProxyLambda);
dbSecret.grantRead(this.dataApiLambda); // 追加
// Data API実行権限を付与 (追加)
this.dataApiLambda.addToRolePolicy(
new iam.PolicyStatement({
actions: [
'rds-data:ExecuteStatement',
'rds-data:BatchExecuteStatement',
'rds-data:BeginTransaction',
'rds-data:CommitTransaction',
'rds-data:RollbackTransaction'
],
resources: [cluster.clusterArn],
})
);
}
}
API Gateway を修正して Data API エンドポイントを追加
lib/apigateway.ts ファイルを修正。
import * as cdk from 'aws-cdk-lib';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
export class ApiGateway extends Construct {
public readonly api: apigateway.RestApi;
constructor(scope: Construct,
id: string,
lambdaFunction: lambda.Function,
dataApiLambda: lambda.Function // 追加
) {
super(scope, id);
// API Gateway
this.api = new apigateway.RestApi(this, 'UsersApi', {
restApiName: 'Users Service',
description: 'API Gateway for Aurora PostgreSQL Users',
deployOptions: {
stageName: 'prod',
},
binaryMediaTypes: []
});
// RDS Proxy Lambda統合
const rdsProxyIntegration = new apigateway.LambdaIntegration(lambdaFunction);
// Data API Lambda統合 (追加)
const dataApiIntegration = new apigateway.LambdaIntegration(dataApiLambda);
// ルートパス(/)にGETメソッド
this.api.root.addMethod('GET', rdsProxyIntegration);
// RDS Proxyエンドポイント
const rdsProxy = this.api.root.addResource('rdsProxy');
rdsProxy.addMethod('GET', rdsProxyIntegration);
// Data APIエンドポイント (追加)
const dataApi = this.api.root.addResource('data-api');
dataApi.addMethod('GET', dataApiIntegration);
}
}
Data API用のLambda関数ソースを定義
lambda/data-api-lambda.tsファイルを作成。
import { RDSDataClient, ExecuteStatementCommand } from '@aws-sdk/client-rds-data';
// 環境変数
const secretArn = process.env.SECRET_ARN;
const dbName = process.env.DB_NAME;
const clusterArn = process.env.CLUSTER_ARN;
const rdsData = new RDSDataClient({ region: process.env.AWS_REGION });
// Lambda関数ハンドラー
export const handler = async (event: any): Promise<any> => {
console.log('Received event:', JSON.stringify(event, null, 2));
try {
// Data APIでクエリを実行
const result = await rdsData.send(
new ExecuteStatementCommand({
resourceArn: clusterArn,
secretArn: secretArn,
database: dbName,
sql: 'SELECT * FROM users',
includeResultMetadata: true,
})
);
// レスポンスデータを整形
const users = result.records?.map(record => {
const user: Record<string, any> = {};
result.columnMetadata?.forEach((meta, index) => {
const columnName = meta.name || `column_${index}`;
const value = record[index].stringValue || record[index].longValue || record[index].booleanValue;
user[columnName] = value;
});
return user;
}) || [];
// 成功レスポンス
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
users: users
})
};
} catch (error: unknown) {
// エラーログ
console.error('Error:', error);
// エラーレスポンス
return {
statusCode: 500,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: 'Error querying users table via Data API',
error: error instanceof Error ? error.message : 'Unknown error'
})
};
}
};
全体
lib/main-stack.ts ファイルを修正。
Aurora クラスター (aurora.cluster)をLambdaクラスのコンストラクタに追加で渡します。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Vpc } from './vpc';
import { Aurora } from './aurora';
import { Lambda } from './lambda';
import { ApiGateway } from './apigateway';
export class MainStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// VPCの作成
const vpc = new Vpc(this, 'VpcConstruct');
// Aurora Serverless v2の作成
const aurora = new Aurora(this, 'AuroraConstruct', vpc.vpc);
// Lambda関数の作成
const lambdaFunction = new Lambda(this,
'LambdaConstruct',
vpc.vpc,
aurora.dbSecret,
aurora.rdsProxy,
aurora.cluster // 追加
);
// API Gatewayの作成
new ApiGateway(this,
'ApiGatewayConstruct',
lambdaFunction.rdsProxyLambda,
lambdaFunction.dataApiLambda //追加
);
}
}
デプロイ
# デプロイ前の構文チェックとCloudFormationテンプレート生成
cdk synth
# デプロイ
cd deploy
動作確認
設定通りに構築されているかコンソールで確認します。
コンソールを開き、AuroraでData APIが有効になっていることを確認。
API GatewayでData API用のリソースが作成されていることを確認。
Data API用のLambda関数が作成されていることを確認。
APIテスト
API Gatewayエンドポイントにリクエストを送信して結果を確認します。
正常に動作すれば、バックエンドの Lambda 関数が SELECT * FROM users クエリを実行し、データベース内の全ユーザー情報が応答として返ってきます。
curl -X GET https://0hvjwvx8s2.execute-api.ap-northeast-1.amazonaws.com/prod/data-api
結果
{
"users": [
{
"id": 1,
"name": "Test User 1",
"create_datetime": "2025-03-17 05:29:33.716686",
"update_datetime": "2025-03-17 05:29:33.716686"
},
{
"id": 2,
"name": "Test User 2",
"create_datetime": "2025-03-17 05:29:33.716686",
"update_datetime": "2025-03-17 05:29:33.716686"
},
{
"id": 3,
"name": "Test User 3",
"create_datetime": "2025-03-17 05:29:33.716686",
"update_datetime": "2025-03-17 05:29:33.716686"
},
{
"id": 4,
"name": "Test User 4",
"create_datetime": "2025-03-17 05:29:33.716686",
"update_datetime": "2025-03-17 05:29:33.716686"
},
{
"id": 5,
"name": "Test User 5",
"create_datetime": "2025-03-17 05:29:33.716686",
"update_datetime": "2025-03-17 05:29:33.716686"
},
{
"id": 6,
"name": "Test User 6",
"create_datetime": "2025-03-17 05:29:33.716686",
"update_datetime": "2025-03-17 05:29:33.716686"
},
{
"id": 7,
"name": "Test User 7",
"create_datetime": "2025-03-17 05:29:33.716686",
"update_datetime": "2025-03-17 05:29:33.716686"
},
{
"id": 8,
"name": "Test User 8",
"create_datetime": "2025-03-17 05:29:33.716686",
"update_datetime": "2025-03-17 05:29:33.716686"
},
{
"id": 9,
"name": "Test User 9",
"create_datetime": "2025-03-17 05:29:33.716686",
"update_datetime": "2025-03-17 05:29:33.716686"
},
{
"id": 10,
"name": "Test User 10",
"create_datetime": "2025-03-17 05:29:33.716686",
"update_datetime": "2025-03-17 05:29:33.716686"
}
]
}
無事、usersテーブルの情報が返ってきたことを確認できました。
おわりに
今回は「AWS CDKで作るサーバーレスデータベース基盤」シリーズ第3回として、RDS Data APIを利用したデータベースアクセスについて解説しました。
今回の構築により、シンプルなサーバーレスAPIを実装することができました。
次回は、RDS ProxyとRDS Data APIのパフォーマンスを比較していく予定です。サーバーレスアプリケーションに最適なデータベース接続方法を選ぶ際の参考になれば幸いです。
最後までお読みいただき、ありがとうございました。