AWS CDKで作るサーバーレスデータベース基盤 【第3回】RDS Data API構築

AWS CDKで作るサーバーレスデータベース基盤 【第3回】RDS Data API構築

Clock Icon2025.03.17

こんにちは!製造ビジネステクノロジー部の小林です。

前回の記事では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シリーズのインスタンスクラスはサポートしていない

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/data-api.html#data-api.regions

RDS Proxy と DataAPIの違いは何か?

RDS Proxy はデータベースへの接続をプールすることでバックエンドのRDSの負荷を軽減しつつ、コネクション作成のコストを軽減します。Data API はデータベースのコネクションではなくAPIを利用してクエリを実行できる機能です。

公式ではこのように説明がありました。

https://aws.amazon.com/jp/blogs/news/onlineseminar-rds-lambda-doc-qa/

前提条件

このハンズオンでは、前回の記事で構築した API Gateway と Lambda を利用します。

  • ハンズオンを実際に試したい方: 前回の記事に沿って環境を構築してください。
  • 概念や実装方法だけを知りたい方: 環境構築なしでも本記事の内容は理解できます。

システム構成図

以下は本シリーズで構築するシステムの全体像です。
今回の記事では、以下のリソースを構築します。

  • Lambda(Data API接続用)
  • Aurora Serverless v2(Data API有効化)

system-diagram

やってみる

※冗長なソースは折りたたんで表示しています。
環境
下記環境で実装しています。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
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
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 ファイルを修正。

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ファイルを作成。

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クラスのコンストラクタに追加で渡します。

main-stack.ts
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が有効になっていることを確認。
スクリーンショット 2025-03-17 14.16.17

API GatewayでData API用のリソースが作成されていることを確認。
スクリーンショット 2025-03-17 14.21.02

Data API用のLambda関数が作成されていることを確認。
スクリーンショット 2025-03-17 14.30.21

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のパフォーマンスを比較していく予定です。サーバーレスアプリケーションに最適なデータベース接続方法を選ぶ際の参考になれば幸いです。
最後までお読みいただき、ありがとうございました。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.