AWS CDKで作るサーバーレスデータベース基盤 【第2回】API Gateway × Lambda × RDS Proxy構築
こんにちは!製造ビジネステクノロジー部の小林です。
前回の記事では Aurora Serverless v2 の構築方法をご紹介しました。
今回は「AWS CDKで作るサーバーレスデータベース基盤」シリーズ第2回として、AuroraにアクセスするAPIシステム全体を構築していきます。
以下のコンポーネントを組み合わせたAPIシステムを構築します。
- Amazon API Gateway
- AWS Lambda
- Amazon RDS Proxy
- Aurora Serverless v2
前提条件
このハンズオンでは、前回の記事で構築したAurora Serverless v2を利用します。
まだ環境をお持ちでない方は、先に前回の記事をご確認ください。
システム構成図
以下は本シリーズで構築するシステムの全体像です。
今回の記事では、以下のリソースを構築します。
- APIGateway
- Lambda(プライベートサブネット内)
- VPCエンドポイント(Secret用)
- RDS Proxy
やってみる
環境
下記環境で実装しています。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)
必要なパッケージのインストール
プロジェクトのルートディレクトリで以下のコマンドを実行します。
# CDKのコアライブラリとコンストラクト
npm install aws-cdk-lib constructs
# TypeScriptコンパイラとツール
npm install --save-dev typescript ts-node
# Node.js型定義
npm install --save-dev @types/node
# Lambda関数の型定義
npm install --save-dev @types/aws-lambda
# AWS SDKとデータベース接続ライブラリ
npm install aws-sdk pg @aws-sdk/client-secrets-manager
# Lambda関数用の依存関係
mkdir -p lambda
cd lambda
# 新しい package.json ファイルをデフォルト設定で初期化
npm init -y
# pg:Lambda関数内でAurora Postgresに接続するために必要
# @aws-sdk/client-secrets-manager: AWS SDK v3のSecretsManagerクライアント
npm install pg @aws-sdk/client-secrets-manager
# @types/pg: pgライブラリのTypeScript型定義
npm install --save-dev @types/pg
# プロジェクトルートに戻る
cd ..
VPCエンドポイントの定義
lib/vpc.ts (既存のコードを使用)
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';
export class Vpc extends Construct {
public readonly vpc: ec2.Vpc;
constructor(scope: Construct, id: string) {
super(scope, id);
this.vpc = new ec2.Vpc(this, 'AuroraVpc', {
maxAzs: 2,
natGateways: 0,
subnetConfiguration: [
{
name: 'Private',
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
cidrMask: 24
}
]
});
// VPC エンドポイント(LambdaがVPC内からシークレットにアクセスできるように)
this.vpc.addInterfaceEndpoint('SecretsManagerEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
privateDnsEnabled: true,
});
// VPC エンドポイント (CloudWatchログ出力用)
this.vpc.addInterfaceEndpoint('CloudWatchLogsEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS,
privateDnsEnabled: true,
});
}
}
RDS Proxyの定義
lib/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) {
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'
);
// 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,
});
// 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関数の定義
lib/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';
export class Lambda extends Construct {
public readonly rdsProxyLambda: lambda.NodejsFunction;
public readonly securityGroup: ec2.SecurityGroup;
constructor(scope: Construct, id: string, vpc: ec2.Vpc, dbSecret: secretsmanager.ISecret,rdsProxy: rds.DatabaseProxy) {
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: 256,
environment: {
PROXY_ENDPOINT: rdsProxy.endpoint,
SECRET_ARN: dbSecret.secretArn,
DB_NAME: 'demodb',
},
bundling: {
minify: true,
nodeModules: ['pg', 'aws-sdk'],
},
});
// Secrets Managerへのアクセス権限を付与
dbSecret.grantRead(this.rdsProxyLambda);
}
}
lambda関数のソースコードを定義
lambda/rds-proxy-lambda.tsファイルを作成。
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
import { Client } from 'pg';
// 環境変数
const secretArn = process.env.SECRET_ARN || '';
const dbName = process.env.DB_NAME || 'demodb';
const proxyEndpoint = process.env.PROXY_ENDPOINT || '';
// SecretsManagerクライアント
const secretsManager = new SecretsManagerClient({ region: process.env.AWS_REGION });
// データベース接続情報を取得する関数
async function getDbCredentials() {
const response = await secretsManager.send(
new GetSecretValueCommand({ SecretId: secretArn })
);
if (response.SecretString) {
return JSON.parse(response.SecretString);
}
throw new Error('Secret string is empty');
}
// Lambda関数ハンドラー
export const handler = async (event: any): Promise<any> => {
console.log('Received event:', JSON.stringify(event, null, 2));
let client: Client | undefined;
try {
// 認証情報を取得
const credentials = await getDbCredentials();
// データベース接続
client = new Client({
host: proxyEndpoint,
port: 5432,
database: dbName,
user: credentials.username,
password: credentials.password,
});
await client.connect();
console.log('Connected to database via RDS Proxy');
// ユーザーデータ取得
const result = await client.query('SELECT * FROM users');
console.log(`Retrieved ${result.rows.length} users`);
// 成功レスポンス
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
users: result.rows
})
};
} catch (error: unknown) {
// エラーログ
console.error('Error:', error);
// エラーレスポンス
return {
statusCode: 500,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: 'Error querying users table',
error: error instanceof Error ? error.message : 'Unknown error'
})
};
} finally {
// 接続を閉じる
if (client) {
await client.end().catch(err => {
console.error('Error closing database connection:', err);
});
console.log('Database connection closed');
}
}
};
API Gatewayの定義
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) {
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: [] // CORSによるOPTIONSの自動作成防止
});
// Lambda統合の作成
const lambdaIntegration = new apigateway.LambdaIntegration(lambdaFunction);
// ルートパス(/)にGETメソッドを追加
this.api.root.addMethod('GET', lambdaIntegration);
// usersエンドポイントの作成
const users = this.api.root.addResource('users');
users.addMethod('GET', lambdaIntegration); // GET /users - すべてのユーザーを取得
}
}
全体
lib/main-stack.tsファイルにLambda関数とAPI Gatewayを追加します。
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 './api-gateway';
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);
// API Gatewayの作成 (ココを追加)
new ApiGateway(this, 'ApiGatewayConstruct', lambdaFunction.rdsProxyLambda);
}
}
デプロイ
# デプロイ前の構文チェックとCloudFormationテンプレート生成
cdk synth
つまづいたポイント:Docker不要のLambda関数ビルド
cdk synth実行時に以下のエラーが発生しました。
Error: spawnSync docker ENOENT
このエラーは、AWS CDKがNodejsFunctionのビルドにDockerを使用しようとしているものの、環境にDockerがインストールされていないことを示しています。
NodejsFunctionコンストラクトは、デフォルトではDockerを使用してLambda関数のコードをバンドルします。
しかし、Docker Desktopを使用していない環境でも開発できるよう、今回はローカルバンドリングを設定することで解決します。ローカルバンドリングについての詳細は以下の記事をご参照ください。
ローカルバンドリング設定
lib/lambda.ts を修正します。
# ...省略
// 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: 256,
environment: {
PROXY_ENDPOINT: rdsProxy.endpoint,
SECRET_ARN: dbSecret.secretArn,
DB_NAME: 'demodb',
},
bundling: {
minify: true,
nodeModules: ['pg'],
forceDockerBundling: false, // ここを追加(Dockerを使用しない)
},
});
# 省略...
# プロジェクトルートで以下を実行。Lambda関数をバンドル
npm install --save-dev esbuild
$ cdk synth
Bundling asset MainStack/LambdaConstruct/RdsProxyLambda/Code/Stage...
cdk.out/bundling-temp-349af78e6134d4e6824d4f76d1f03d3252752308383959df27428f92fa729715/index.js 1.7kb
⚡ Done in 2ms
これは esbuild が Lambda 関数のコードをバンドルしている出力です。
esbuild は非常に高速なバンドラーで、この例では Lambda関数のコードとその依存関係を単一のJavaScriptファイル(index.js)にまとめています。このバンドルされたコードが AWS Lambda にデプロイされます。
バンドル処理が正常に完了したのでデプロイします。
# デプロイ
cdk deploy
結果
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/MainStack/e6eeda60-0214-11f0-bc0f-06cd69efd575
✨ Total time: 771.35s
動作確認
デプロイが完了したら、以下の3ステップで動作確認を行います。
- リソースの確認: AWSコンソールで各リソースが正しく構築されているか確認
- テストデータの準備: Aurora DBにテストテーブルとデータを作成
- APIテスト: API Gatewayエンドポイントにリクエストを送信して結果を確認
1. リソースの確認
API Gateway
Lambda
シークレットへのアクセス権限
環境変数
VPC、セキュリティグループ
RDS Proxy
VPCエンドポイント(Secret用)
2. テストデータの準備
次にクエリエディタでAuroraにテーブルを作成します。
テーブルを作成
データを挿入
3. APIテスト
API Gatewayエンドポイントにリクエストを送信して結果を確認します。
正常に動作すれば、バックエンドのLambda関数がSELECT * FROM users クエリを実行し、データベース内の全ユーザー情報が応答として返ってきます。
curl -X GET https://rdc6d9p27c.execute-api.ap-northeast-1.amazonaws.com/prod
結果
{
"users": [
{
"id": 1,
"name": "Test User 1",
"create_datetime": "2025-03-16T15:48:22.135Z",
"update_datetime": "2025-03-16T15:48:22.135Z"
},
{
"id": 2,
"name": "Test User 2",
"create_datetime": "2025-03-16T15:48:22.135Z",
"update_datetime": "2025-03-16T15:48:22.135Z"
},
{
"id": 3,
"name": "Test User 3",
"create_datetime": "2025-03-16T15:48:22.135Z",
"update_datetime": "2025-03-16T15:48:22.135Z"
},
{
"id": 4,
"name": "Test User 4",
"create_datetime": "2025-03-16T15:48:22.135Z",
"update_datetime": "2025-03-16T15:48:22.135Z"
},
{
"id": 5,
"name": "Test User 5",
"create_datetime": "2025-03-16T15:48:22.135Z",
"update_datetime": "2025-03-16T15:48:22.135Z"
},
{
"id": 6,
"name": "Test User 6",
"create_datetime": "2025-03-16T15:48:22.135Z",
"update_datetime": "2025-03-16T15:48:22.135Z"
},
{
"id": 7,
"name": "Test User 7",
"create_datetime": "2025-03-16T15:48:22.135Z",
"update_datetime": "2025-03-16T15:48:22.135Z"
},
{
"id": 8,
"name": "Test User 8",
"create_datetime": "2025-03-16T15:48:22.135Z",
"update_datetime": "2025-03-16T15:48:22.135Z"
},
{
"id": 9,
"name": "Test User 9",
"create_datetime": "2025-03-16T15:48:22.135Z",
"update_datetime": "2025-03-16T15:48:22.135Z"
},
{
"id": 10,
"name": "Test User 10",
"create_datetime": "2025-03-16T15:48:22.135Z",
"update_datetime": "2025-03-16T15:48:22.135Z"
}
]
}
無事usersテーブルの情報が返ってきたことが確認できました。
RDS Proxyの動作確認
AWS RDS Proxy のモニタリングメトリクスを確認すると、サーバーレスアプリケーションが正常に機能していることが確認できました。
1. QueryRequests(クエリリクエスト数)
- 値が2に上昇しているのは、Lambda関数から実行されたSQLクエリの数を示しています。
- 1回目:「接続できてる?」というテスト
- 2回目:「このデータを取得して」というSELECTクエリ
2. ClientConnections(クライアント接続数)
- アプリケーション(Lambda)からRDS Proxyへの接続数です。
- 値が低いままなのは良いことで、これはRDS Proxyが賢く接続を使い回しているからです。
3. DatabaseConnections(データベース接続数)
- RDS ProxyからAuroraデータベースへの接続数です。
- 値が少しずつ2まで増えているのは、RDS Proxyが必要に応じて接続を作り、それを再利用しているからです。
これらのメトリクスから、設計したアーキテクチャ(API Gateway → Lambda → RDS Proxy → Aurora)が意図した通りに動作していることが確認できました。
おわりに
今回は「AWS CDKで作るサーバーレスデータベース基盤」シリーズ第2回として、以下のコンポーネントをAWS CDKで構築・連携する方法を解説しました。
- Amazon API Gateway
- AWS Lambda
- Amazon RDS Proxy
- VPC Endpoint
次回は、RDS Data APIを利用したデータベースアクセスについて解説します。Lambdaからクエリを実行し、Aurora Serverless v2に接続するフローを見ていく予定です。
最後までお読みいただき、ありがとうございました。
参考にした資料