AWS CDKで作るサーバーレスデータベース基盤 【第2回】API Gateway × Lambda × RDS Proxy構築

AWS CDKで作るサーバーレスデータベース基盤 【第2回】API Gateway × Lambda × RDS Proxy構築

Clock Icon2025.03.16

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

前回の記事では 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
    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)

必要なパッケージのインストール
プロジェクトのルートディレクトリで以下のコマンドを実行します。

# 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 (既存のコードを使用)

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 (既存のコードを使用)

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

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

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

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を追加します。

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 './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を使用していない環境でも開発できるよう、今回はローカルバンドリングを設定することで解決します。ローカルバンドリングについての詳細は以下の記事をご参照ください。
https://dev.classmethod.jp/articles/local-build-a-lambda-function-nodejs-without-docker-desktop-with-aws-cdk/

ローカルバンドリング設定
lib/lambda.ts を修正します。

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ステップで動作確認を行います。

  1. リソースの確認: AWSコンソールで各リソースが正しく構築されているか確認
  2. テストデータの準備: Aurora DBにテストテーブルとデータを作成
  3. APIテスト: API Gatewayエンドポイントにリクエストを送信して結果を確認

1. リソースの確認

API Gateway
スクリーンショット 2025-03-17 0.06.39

Lambda
シークレットへのアクセス権限
スクリーンショット 2025-03-17 0.09.50

環境変数
スクリーンショット 2025-03-17 0.12.04

VPC、セキュリティグループ
スクリーンショット 2025-03-17 0.17.00

RDS Proxy
スクリーンショット 2025-03-17 0.21.40

VPCエンドポイント(Secret用)
スクリーンショット 2025-03-17 0.20.02

2. テストデータの準備

次にクエリエディタでAuroraにテーブルを作成します。
スクリーンショット 2025-03-17 0.33.42

テーブルを作成
スクリーンショット 2025-03-17 0.45.11

データを挿入
スクリーンショット 2025-03-17 0.48.36

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 のモニタリングメトリクスを確認すると、サーバーレスアプリケーションが正常に機能していることが確認できました。
スクリーンショット 2025-03-17 1.39.20

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に接続するフローを見ていく予定です。

最後までお読みいただき、ありがとうございました。

参考にした資料

https://dev.classmethod.jp/articles/aws-cdk-connect-to-amazon-aurora-db-cluster-from-lambda-function-via-rds-proxy/
https://dev.classmethod.jp/articles/cdk-rds-proxy-api-go/
https://dev.classmethod.jp/articles/local-build-a-lambda-function-nodejs-without-docker-desktop-with-aws-cdk/

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.