Serverless ExpressからDSQLに接続する

Serverless ExpressからDSQLに接続する

2025.10.31

はじめに

Amazon Aurora DSQL(以下、DSQL)は、サーバレスの分散データベースです。PostgreSQLと互換性があり、アプリケーションからは使い慣れたドライバーやフレームワーク、ORMを使用できます。この記事では、Serverless ExpressからDSQLクラスターに接続し、データの取得や登録ができることを確認します。

前提

  • Node.jsがインストールされていること
  • AWS CDK v2がインストールされていること

注意点

Serverless ExpressからDSQLに接続することに焦点を当てているため、アプリケーションコードは動かすための最低限の実装となっています。実運用で使用するアプリケーションを作成する際は、バリデーションチェックや型安全性、エラーハンドリング等の適切な実装が必要です。

ライブラリのインストール

プロジェクトフォルダを用意し、backendinfraディレクトリを作成します。

dsql-lambda/
├── backend/
└── infra/

backendディレクトリに移動し、Serverless Expressをインストールします。

npm install @codegenie/serverless-express

その他必要なライブラリをインストールします。

npm install @aws-sdk/dsql-signer express pg
npm install -D @types/express @types/node @types/pg

backendディレクトリ直下のpackage.jsonは以下のようになります。

{
  "dependencies": {
    "@aws-sdk/dsql-signer": "^3.883.0",
    "@codegenie/serverless-express": "^4.17.0",
    "express": "^5.1.0",
    "pg": "^8.16.3"
  },
  "devDependencies": {
    "@types/express": "^5.0.3",
    "@types/node": "^24.3.1",
    "@types/pg": "^8.15.5"
  }
}

backendディレクトリ直下にtsconfig.jsonを作成します。

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "types": ["node"]
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}

以上でbackendディレクトリへのインストールは完了です。

続いてinfraディレクトリに移動し、CDKプロジェクトのセットアップをします。

cdk init app --language typescript

インストール中に自動的にGitリポジトリの設定が行われますが、不要な場合はinfraディレクトリに作成された.gitディレクトリを削除します。

データベースクライアントの作成

pgライブラリを使用してDSQLクラスターに接続するためのモジュールを作成します。

backend/shared/db/client.tsファイルを作成します。

import { DsqlSigner } from "@aws-sdk/dsql-signer";
import { Pool, PoolClient, QueryResult } from "pg";

const region = process.env.REGION || "";
const clusterEndpoint = process.env.DSQL_CLUSTER_ENDPOINT || "";

if (!region) {
  throw new Error("Region is not set.");
}
if (!clusterEndpoint) {
  throw new Error("DSQL endpoint is not set.");
}

/**
 * トークン取得
 */
const getToken = async (): Promise<string> => {
  const signer = new DsqlSigner({ hostname: clusterEndpoint, region });
  return signer.getDbConnectAuthToken();
};

const dsqlPool = new Pool({
  host: clusterEndpoint,
  port: 5432,
  database: "postgres",
  user: "api_executor",
  password: getToken,
  ssl: { rejectUnauthorized: true },
  maxLifetimeSeconds: 60 * 50,
  idleTimeoutMillis: 1000 * 60 * 50,
});

/**
 * クエリ実行
 */
export const query = (
  text: string,
  params?: unknown[],
): Promise<QueryResult> => {
  return dsqlPool.query(text, params);
};

/**
 * クライアント取得(トランザクション用)
 */
export const getClient = async (): Promise<PoolClient> => {
  return dsqlPool.connect();
};

export default dsqlPool;

以下の記事を参考に、最大接続時間超過エラーが発生しないよう、maxLifetimeSecondsで50分までしか接続が保持されないように設定しています。

DSQLの最大接続時間超過エラーが発生しないLambdaの実装について検証してみた | DevelopersIO

トランザクション処理の場合はトランザクション開始~クエリ実行~コミットが同一セッションである必要があるため、Poolqueryではなく、取得した専用クライアントを使用するようにします。

DSQLはIAMベースの認証を使用します。接続時には以下の流れで認証が行われます。

  1. DsqlSignerを使用して一時的な認証トークンを取得
  2. このトークンをパスワードとして使用し、PostgreSQL接続を確立
  3. 接続プールが自動的にトークンを更新

トークンには有効期限があるため、pgPoolpasswordに関数を渡すことで、接続時に毎回新しいトークンを取得します。

アプリケーションの作成

この記事では、

  • ユーザIDを元にユーザ名を取得する
  • ユーザを登録する

という2つのAPIを作成します。

backend/service/user.tsを作成します。

import dsqlPool, { getClient } from "../shared/db/client";

export const getUserName = async (userId: number): Promise<string | null> => {
  const result = await dsqlPool.query(`SELECT name from users where user_id = $1`, [userId]);
  const rows = result.rows;
  if (rows.length === 0) {
    return null
  }
  return rows[0].name;
};

export const registerUser = async (userId: number, name: string): Promise<void> => {
  const client = await getClient();

  try{
    await client.query('BEGIN')
    await client.query(`INSERT INTO users(user_id, name) VALUES ($1, $2)`, [userId, name]);
    await client.query('COMMIT')
  } catch (error) {
    await client.query('ROLLBACK');
  } finally {
    client.release()
  }
};

ユーザ名を取得する処理は、Poolqueryを使用します。一方で、ユーザを登録する処理は専用のクライアントを使用します。

続いてこの処理を呼び出すハンドラー、ルーターを作成していきます。

backend/handler/get-user.ts

import { getUserName } from '../service/user';

type GetUserHandlerProps = {
  userId: number
}

export const handler = async (props: GetUserHandlerProps) => {
  const name = await getUserName(props.userId);

  return {
    statusCode: 200,
    body: {
      userId: props.userId,
      name: name ?? "Not Found",
      timestamp: new Date().toISOString()
    }
  }
}

backend/handler/post-user.ts

import { registerUser } from '../service/user';

type PostUserHandlerProps = {
  userId: number
  name: string
}

export const handler = async (props: PostUserHandlerProps) => {
  await registerUser(props.userId, props.name);

  return {
    statusCode: 201,
    body: {}
  }
}

backend/app.ts

import serverlessExpress from '@codegenie/serverless-express';
import express, { Request, Response } from 'express';
import { handler as getUserHandler } from './handler/get-user';
import { handler as postUserHandler } from './handler/post-user';

const app = express();
app.use(express.json());

app.get('/users/:userId', async (req: Request, res: Response): Promise<void> => {
  const userId = Number(req.params.userId)
  if (!Number.isNaN(userId)) {
    const result = await getUserHandler({ userId: userId });
    res.status(result.statusCode).send(result.body);
  } else {
    res.status(400).send({
      message: "Validation error"
    });

  }
});

app.post('/users', async (req: Request, res: Response): Promise<void> => {
  const userId = Number(req.body.userId)
  const name = req.body.name
  if(!Number.isNaN(userId)){
    const result = await postUserHandler({ userId: Number(userId), name });
    res.status(result.statusCode).send(result.body);
  } else {
    res.status(400).send({
      message: "Validation error"
    });
  }
});

export const handler = serverlessExpress({ app });

AWSリソースの作成

CDKプロジェクトのセットアップをすると、デフォルトでinfra/lib/infra-stack.tsが作成されます。

ファイルの内容を以下のように変更し、DSQLクラスター、Lambda関数、API Gatewayを作成します。

import {
  aws_lambda,
  aws_lambda_nodejs,
  aws_apigateway,
  aws_iam,
  Duration,
  CfnOutput,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';
import * as dsql from "aws-cdk-lib/aws-dsql";

export class InfraStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const dsqlCluster = new dsql.CfnCluster(this, "DsqlCluster");

    const apiFunction = new aws_lambda_nodejs.NodejsFunction(
      this,
      'BackendApiFunction',
      {
        runtime: aws_lambda.Runtime.NODEJS_22_X,
        entry: "../backend/app.ts",
        timeout: Duration.seconds(60),
        environment: {
          REGION: this.region,
          DSQL_CLUSTER_ENDPOINT: `${dsqlCluster.attrIdentifier}.dsql.${this.region}.on.aws`
        },
      }
    );
    apiFunction.addToRolePolicy(
      new aws_iam.PolicyStatement({
        effect: aws_iam.Effect.ALLOW,
        actions: [
          'dsql:DbConnect'
        ],
        resources: [dsqlCluster.attrResourceArn]
      })
    )

    /**
     * Create API
     */
    const api = new aws_apigateway.LambdaRestApi(this, 'BackendApi', {
      handler: apiFunction,
      deployOptions: {
        stageName: 'v1',
      },
    });

    /**
     * Create API Gateway Endpoint
     */
    new CfnOutput(this, 'ApiEndpoint', {
      value: api.deploymentStage.urlForPath(),
    });
  }
}

infraディレクトリに移動し、リソースをデプロイします。

cdk deploy

コンテナイメージのビルドが失敗する場合、esbuildをインストールします。

npm install esbuild

DSQLクラスターの設定

リソースのデプロイが完了したら、続いてDSQLクラスターを設定します。

DSQLのページでリソースが作成されていることを確認します。

20251031_d_01

クラスターIDのリンクをクリックし、「接続」⇒「CloudShellで開く」を選択します。

20251031_d_02

デフォルトのままで「CloudShellを起動」をクリックします。

20251031_d_03

CloudShellが起動したら、テーブルを作成します。

CREATE TABLE users (
    user_id INTEGER PRIMARY KEY,
    name TEXT NOT NULL
);

DSQLはIAMロールまたはIAMユーザを使用して認証します。今回は、Lambdaの実行ロールに対してデータベースへのアクセスを許可します。

なお、Lambdaの実行ロールはLambda関数の「設定」タブ⇒「アクセス権限」を選択し、記載されたリンクをクリックした先で確認できます。

20251031_d_04

20251031_d_05

まず、データベースロールを作成します。データベースロールの名前は、データベースクライアントでPoolを作成するときに指定したユーザ名です。

CREATE ROLE api_executor WITH LOGIN;

続いてデータベースロールとIAMロールを関連付けます。

AWS IAM GRANT api_executor TO 'Lambda実行ロールのARN';

最後に、データベースロールに対してテーブルの操作権限を与えます。

GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO api_executor;

なお、将来作成されるテーブルに対しても操作権限を付与するALTER DEFAULT PRIVILEGESコマンドは、記事執筆時点ではサポートされていないのでご注意ください。

参考

データベースロールと IAM 認証の使用 - Amazon Aurora DSQL

なお、上記設定は、一般ユーザ向けのアクセス権限となっています。管理者向けの権限でアクセスしたい場合、接続ユーザ名をadmin、Lambda関数に付与するポリシーをdsql:DbConnectAdminに設定し、接続トークンを取得するためにDsqlSignerのgetDbConnectAdminAuthToken関数を使用します。

動作確認

実際にAPIを実行して動作確認します。

APIのエンドポイントはCloudFormationの「出力」タブで確認できます。

20251031_d_06

ユーザを登録するため、以下のcurlコマンドを実行します。

curl -X POST https://<APIエンドポイント>/users \
  -H "Content-Type: application/json" \
  -d '{"userId": 1, "name": "Ken"}'

登録したユーザを取得するため、以下のcurlコマンドを実行します。

curl https://<APIエンドポイント>/users/1

以下のように結果が取得でき、Serverless ExpressからDSQLにアクセスできていることが確認できました。

{"userId":1,"name":"Ken","timestamp":"2025-10-30T14:25:49.382Z"}

おわりに

この記事では、Serverless ExpressからAmazon Aurora DSQLへの接続方法を確認しました。

主なポイントは以下の通りです。

  • DSQLはIAMベースの認証であり、一時的なトークンを取得して接続する
  • IAMロールまたはIAMユーザとデータベースロールを関連付ける
  • ALTER DEFAULT PRIVILEGESコマンドがサポートされていないため、テーブル追加ごとに権限付与が必要

DSQLはインフラ管理不要を謳っているだけあり、設定項目も非常に少なく、サクッと動作確認ができました。

この記事がどなたかの参考になれば幸いです。

参考

分散 SQL データベース – Amazon Aurora DSQL – AWS

この記事をシェアする

FacebookHatena blogX

関連記事