AWS CDKによるAurora PostgreSQL Serverless v2およびRDS Data APIの環境構築

Aurora Serverless v2のPostgreSQL互換でRDS Data APIが使えるようになった。

最小ACUを0にできないなど、Serverless v1と完全互換ではないが、ちょうどSalesforceやHTTPでAPI連携できるRDBを使いたい要件があったので、検証することに。

Secrets Managerへの認証情報の登録など、RDS Data APIを使うには準備が必要だが、AWS CDKで環境構築すると自動でやってくれたのでメモ。

環境

AWS CDK Toolkit v2.133.0、AWS CLI v2.15.30。

AWS CDKプロジェクトの初期化

AWS CDKのインストール等は省略。以下のコマンドでプロジェクトを初期化。

mkdir -p aurora-serverless-example/backend
cd aurora-serverless-example/backend

cdk init app --language typescript

git add package-lock.json
git commit -m 'add package-lock.json'

Stackの記述

作成された aurora-serverless-example/backend/lib/backend-stack.ts に以下を記述。

import { Stack, StackProps } from 'aws-cdk-lib'
import { IpAddresses, SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2'
import { AccessKey, User } from 'aws-cdk-lib/aws-iam'
import {
  AuroraPostgresEngineVersion,
  ClusterInstance,
  DatabaseCluster,
  DatabaseClusterEngine,
} from 'aws-cdk-lib/aws-rds'
import { StringParameter } from 'aws-cdk-lib/aws-ssm'

import { Construct } from 'constructs'

export class BackendStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props)

    // VPC
    const vpc = new Vpc(this, 'Vpc', {
      vpcName: 'AuroraServerlessExample',
      ipAddresses: IpAddresses.cidr('172.30.0.0/16'),
      maxAzs: 2,
      subnetConfiguration: [
        {
          cidrMask: 24,
          subnetType: SubnetType.PRIVATE_ISOLATED,
          name: 'PrivateIsolated',
        },
      ],
    })

    // RDS
    const databaseCluster = new DatabaseCluster(this, 'Aurora', {
      engine: DatabaseClusterEngine.auroraPostgres({
        version: AuroraPostgresEngineVersion.VER_15_5,
      }),
      serverlessV2MinCapacity: 0.5,
      serverlessV2MaxCapacity: 1,
      writer: ClusterInstance.serverlessV2('writer'),
      readers: undefined,
      enableDataApi: true,
      iamAuthentication: true,
      storageEncrypted: true,
      defaultDatabaseName: 'aurora_serverless_example',
      vpc,
      vpcSubnets: vpc.selectSubnets({
        subnetType: SubnetType.PRIVATE_ISOLATED,
      }),
    })

    // IAM User
    const iamUser = new User(this, 'User')
    databaseCluster.grantDataApiAccess(iamUser)
    databaseCluster.secret?.grantRead(iamUser)

    // AccessKey
    const accessKey = new AccessKey(this, 'AccessKey', {
      user: iamUser,
    })

    const ssmAccessKeyId = 'AccessKeyId'
    new StringParameter(this, ssmAccessKeyId, {
      parameterName: ssmAccessKeyId,
      stringValue: accessKey.accessKeyId,
    })

    const ssmSecretAccessKey = 'SecretAccessKey'
    new StringParameter(this, ssmSecretAccessKey, {
      parameterName: ssmSecretAccessKey,
      stringValue: accessKey.secretAccessKey.unsafeUnwrap(),
    })
  }
}

検証用なのでいろいろ適当だが、以下解説。

VPCの作成

RDS Data APIはエンドポイント経由で実行するためか、サブネットタイプは PRIVATE_ISOLATED でよかった。

AZは2以上指定しないと、DatabaseCluster生成時にエラーが発生するため、最小値の2としている。(東京リージョンで subnetConfiguration に1種類しか指定しない場合、 maxAzs を指定しなくてもAZは2つになる模様)

DatabaseClusterの作成

検証のため、ACUはMin/Maxそれぞれ最小値にし、リーダーインスタンスは未設定。なお、RDS Data APIは実行するSQLの種類によらず、ライターインスタンス経由で実行される模様。

docs.aws.amazon.com

enableDataApitrue を指定すると、RDS Data APIが有効となる。また、上記リンクやこちらにあるように、RDS Data APIの実行にはSecrets Managerにデータベース側のユーザーの認証情報が必要だが、CDKを使うと DatabaseCluster の第3引数の credentials で指定した認証情報が、自動的にSecrets Managerに保存される。

上記のコードでは credentials を省略しているため、ユーザー名はデフォルトの postgres となる。別のユーザー名を指定したい場合、以下のように記述可能。

import { Credentials } from 'aws-cdk-lib/aws-rds'

new DatabaseCluster(this, 'Aurora', {
  credentials: Credentials.fromGeneratedSecret('userName'),
}

iamAuthentication にも true を指定しているが、これはIAM データベース認証を有効化するかのプロパティ。RDS Data APIの実行時には、Signature Version 4による署名が必須なので、IAMユーザーが必要となる。

IAMユーザーの作成

RDS Data APIの実行に必要な権限を持ったIAMユーザーを作成する。検証なので、直接IAMユーザーに権限追加している。

RDS Data API へのアクセスの承認には、

AWS 管理ポリシー AmazonRDSDataFullAccess には、Data API のアクセス許可が含まれています。

との記載があるが、以下のように AmazonRDSDataFullAccess ポリシーを付与しても、RDS Data API実行時にSecrets Managerの読み取り権限なしでエラーとなる。

new User(this, 'User', {
  managedPolicies: [
    ManagedPolicy.fromAwsManagedPolicyName('AmazonRDSDataFullAccess'),
  ],
})

原因は、 AmazonRDSDataFullAccess 内で読み取り可能なシークレットのARNが arn:aws:secretsmanager:*:*:secret:rds-db-credentials/* に限定されているため。DatabaseClusterのcredentialsから作成されたシークレットのARNは、 arn:aws:secretsmanager:*:*:secret:<DatabaseClusterのID>Secret<ランダム文字列8桁> となるため、一致しない。

credentialsから作成されたシークレットは databaseCluster.secret で参照可能なので、 databaseCluster.grantDataApiAccess(iamUser) でDBクラスターへのRDS Data API実行権限、 databaseCluster.secret?.grantRead(iamUser) でシークレットの読み取り権限をそれぞれIAMユーザーに追加した。

アクセスキーの作成

AWS Signature Version 4で使用するアクセスキーを作成し、AccessKeyIdとSecretAccessKeyをそれぞれ保存する。

SecretAccessKeyはSecrets Managerに保存したほうがいいのだろうが、検証なのでAccessKeyIdと同じくSystems Managerに保存している。

デプロイ

AWS CLIでログインし、 cdk synth および cdk deploy でデプロイ。東京リージョンへのデプロイの場合、15分弱で完了した。

動作確認

RDSのクエリエディタはRDS Data APIを用いてSQLを実行するため、クエリエディタを用いることで動作確認可能(IAMにRDS Data APIの実行権限およびシークレットの参照権限がある前提)。

cdk.out/BackendStack.template.json を参照するかSecrets Managerを管理コンソールで開くなどして、作成されたシークレットのARNを確認しておく。

AWSの管理コンソールからRDSを開き、左フレームの「クエリエディタ」をクリック。以下の手順でクエリエディタを起動。

  1. 作成したDBクラスターを選択
  2. 「データベースユーザー名」で「Secrets Manager ARN と接続する」を選択
  3. 「Secrets manager ARN」に、確認したシークレットのARNを設定
  4. 「データベースの名前を入力」に、DB名を設定。今回は aurora_serverless_example

以下のSQLで、DDLおよびDMLが実行できることを確認する。

CREATE SCHEMA example;

CREATE TABLE example.users (
 user_id BIGSERIAL NOT NULL,
 user_name VARCHAR(50) NOT NULL,
 PRIMARY KEY (user_id)
);

INSERT INTO example.users (user_name) VALUES ('user1');
INSERT INTO example.users (user_name) VALUES ('user2');
INSERT INTO example.users (user_name) VALUES ('user3');

SELECT user_id, user_name FROM example.users ORDER BY user_id;

なお、クエリエディタではステートメントごとに別リクエストで実行されるのか、 SET search_path TO example; などを挟んでも効果がないため、それぞれスキーマを指定している。

振り返り

初CDKなので、これでいいのやら。ただ、シークレットを自動で設定してくれたりと、便利なのは間違いない。

昔CloudFormationを手書きしていたのに比べると、格段に楽になったなぁ。