CDKでREST API×RDBシステムを構築する 2. 実装後半編
Agaroot IT Partners(AITP)のtomoです。
前回の記事に続いて、CDKでREST API×RDBシステムを構築していこうと思います。
前回の振り返り
前回では、構築予定のシステム概要と実装の前半部分(スタックやVPCの実装)についてお伝えしました。
今回も下記GitHubリポジトリのソースをもとに説明していきますので、適宜参照してください。
https://github.com/aitp-tomo/demo-api
実装
RDB/Secrets Manager
DBやその接続情報の作成です。主なソースは下記になります。
- https://github.com/aitp-tomo/demo-api/blob/main/lib/wrapper/DBWrapper.ts
- https://github.com/aitp-tomo/demo-api/blob/main/lib/wrapper/SecretWrapper.ts
といっても、本記事では(タイトルに「RDB」と書いていながら何ですが)簡単な注意事項の記載のみにとどめようと思います。Aurora Serverless v2の立ち上げ方法などは既存のウェブ記事などで紹介されているからです。今回のシステムでも下記記事を参考にさせていただきました。
https://www.skyarch.net/blog/ついに解禁!aws-cdk-v2-82-0でaurora-serverless-v2を利用可能に!/
注意点は2点ございます。
第一にACUの設定についてです。これは何もCDKを用いているからという訳ではありませんが、最小ACUは2 ACU以上に設定すべきです。
第二に、CDKでAurora Serverless v2を立ち上げようとする際、証明書を明記しておかないと古い証明書が使用されて後々で更新しないといけなかったりするので、証明書を明記しておきましょう。
ちなみにですが、今回はクラスターのパラメータグループによりタイムゾーンや文字コードを指定しているので、登録日時や更新日時はJSTで登録され、文字列データに絵文字なども使えます。
const charSet = "utf8mb4";
this.cluster = new rds.DatabaseCluster(this.scope, this.clusterId, {
// 中略
parameters: {
time_zone: "Asia/Tokyo",
character_set_client: charSet,
character_set_connection: charSet,
character_set_database: charSet,
character_set_results: charSet,
character_set_server: charSet,
innodb_file_per_table: "1",
"skip-character-set-client-handshake": "1",
init_connect: `SET NAMES ${charSet}`,
},
});そのあとはSecrets Managerのシークレットを作成していきます。adminユーザのシークレットはDBを作成した際に自動で作成されますので他のユーザのシークレットのみ作成してください。
Lambda関数/Cognito/API Gateway
処理を担うLambda関数の作成やAPIの設定を行っていきます。今回はビジネス要件で頻出する、ユーザの認証も行います。主なソースは下記の通りです。
- https://github.com/aitp-tomo/demo-api/blob/main/lib/wrapper/LambdaWrapper.ts
- https://github.com/aitp-tomo/demo-api/blob/main/lib/wrapper/AuthorizerWrapper.ts
- https://github.com/aitp-tomo/demo-api/blob/main/lib/wrapper/ApiWrapper.ts
Lambda関数やAPI Gatewayについては過去の記事で比較的詳細に取り扱ったので、今回はCognitoとの連携についてフォーカスします。

CognitoとAPI Gatewayを連携する場合にはオーソライザーを作成する必要があります。オーソライザーとは何かを説明する前に、前提知識となるユーザープールについて触れていきましょう。
ユーザープールはCognitoでユーザー情報を管理したり、サインインやアクセス制御などの認証処理を行うサービスです。ざっくりいうとユーザー管理、認証周りに必要なものをひとまとめにしたもの、と捉えれば問題ないかと思います。
さて、オーソライザーとは何かという話ですが、API Gatewayの機能の1つで、API呼び出しに認証設定を付与することができます。オーソライザーの認証元にはいくつか選択肢があるのですが、今回はCognitoユーザープールを使用していきます。この方法なら設定も簡単かつユーザープールの方で権限などをきめ細やかに設定できるので、業務で用いるシステムには適しているでしょう。
また、今回はユーザープールやオーソライザーとは別にユーザープールクライアントも作成しています。ユーザープールクライアントはユーザープールの一機能であり、ウェブアプリやモバイルアプリでの認証処理の設定を行うことができます。
ユーザープールクライアントを作成すれば、例えばAmplify UIを用いて簡単にサインイン/サインアップフォームをフロントエンド上に配置したり、必要に応じてMFAの設定やソーシャルプロバイダ(GoogleやFaceBookなど)を用いたサインイン/サインアップを実現したりできます。
REST APIのシステムのユースケースとして、フロントエンドとの疎通が多いかと思いましたのでユーザープールクライアントも作成しました。
オーソライザーを用いた認証をAPIで実現するには、APIにオーソライザーをアタッチしてAPIメソッドの設定の方でもオプションにオーソライザーに関する設定を追加する必要があります。
private createAuthorizer = (): void => {
const authorizerId = `${this.appId}-authorizer`;
this.authorizer = new apigateway.CognitoUserPoolsAuthorizer(
this.scope,
authorizerId,
{
cognitoUserPools: [this.userPool],
authorizerName: authorizerId,
identitySource: "method.request.header.Authorization",
}
);
this.authorizer._attachToApi(this.api);
}; private readonly createOptions = (): void => {
this.optionsWithAuthorizer = {
authorizationType: apigateway.AuthorizationType.COGNITO,
authorizer: {
authorizerId: this.authorizerWrapper.authorizer.authorizerId,
},
};
};
// 中略
private readonly addResource = (
resource: apigateway.IResource,
pathPart: string,
apiMethods: ApiMethod[]
): apigateway.Resource => {
const newResource = resource.addResource(pathPart);
apiMethods.forEach((apiMethod) => {
const integration = new apigateway.LambdaIntegration(apiMethod.lambda);
const options = apiMethod.withAuthorizer
? this.optionsWithAuthorizer
: undefined;
newResource.addMethod(apiMethod.methodOption, integration, options);
});
return newResource;
};踏み台インスタンス
今回のソースについて触れる前に、1つ寄り道します。CDKで踏み台インスタンスを作成するのであれば専用のコンストラクトがあるのでそちらを紹介します。BastionHostLinuxという名前のコンストラクトです。
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.BastionHostLinux.html
一から実装するのであれば上記を用いた方が良いかと思いますが、今回のソースでは私が以前用いたことのある方法で実装していきます。
https://github.com/aitp-tomo/demo-api/blob/main/lib/wrapper/BastionWrapper.ts
L1のCfnInstanceを用いてインスタンスを作成しています。IAMインスタンスプロフィールを指定してセッションマネージャーから接続出来るようにしているほか、ユーザーデータを用いてインスタンス作成時mysqlコマンドを自動インストールするようにしています。
const instanceType = ec2.InstanceType.of(
ec2.InstanceClass.T2,
ec2.InstanceSize.MICRO
);
const machineImage = ec2.MachineImage.latestAmazonLinux2023();
const instanceName = `${this.bastionId}-instance`;
new ec2.CfnInstance(this.scope, instanceName, {
availabilityZone: this.vpcWrapper.bastionSubnet.availabilityZone,
disableApiTermination: false,
instanceType: instanceType.toString(),
iamInstanceProfile: this.profile.instanceProfileName!,
imageId: machineImage.getImage(this.scope).imageId,
networkInterfaces: [
{
deviceIndex: "0",
associatePublicIpAddress: true,
deleteOnTermination: true,
associateCarrierIpAddress: false,
groupSet: [this.vpcWrapper.bastionSecurityGroup.securityGroupId],
subnetId: this.vpcWrapper.bastionSubnet.subnetId,
},
],
tags: [
{
key: "Name",
value: instanceName,
},
],
userData: fs.readFileSync(
join(__dirname, "..", "assets", "userData.sh"),
"base64"
),
});ここで注意いただきたいのは、終了保護(disableApiTermination)を有効化してはいけないことです。
上記コードでAMIをマイナーバージョン含めて完全に指定しない限り、マイナーアップデートがある度にインスタンスの切り替え処理が行われます。古いインスタンスが終了し、新しいバージョンのインスタンスが新規作成される訳です。
しかし、終了保護が指定されていると古いバージョンのインスタンスが終了せず、放っておくと踏み台インスタンスが複数台起動されてしまいます。
アプリが動くインスタンスならまだしも、踏み台インスタンスであればその場その場での操作しか行われず、1つのファイルを長時間保存したりしない筈(そしてそうあるべき)かと思いますので、終了保護を無効化してインスタンスが複数台作成されないようにしましょう。
余談ですが、L2だとまだ終了保護を有効化したインスタンスの作成ができないです。
信じられない人もいるかと思いますが、終了保護を有効化する場合にはL1で作成しましょう。
次回は動作確認編
動作確認方法などは別記事にて紹介いたします。
そちらではPostmanを用いて直接APIを実行してみる方法の他、簡易的なReactアプリを用いてフロントエンドから呼び出してみる方法などを紹介します。
是非ご期待ください。
追記
続きが公開されたので、是非ご一読ください。