CDKでREST API×RDBシステムを構築する 1. 実装前半編

Agaroot IT Partners(AITP)のtomoです。

最近では、REST APIを用いてRDBなどからデータを取得したり登録したりするシステムを構築するのが当たり前になってきたのではないでしょうか。寧ろGraphQLやBaaS (Backend As A Service)などの代替案もあり、そちらが大変人気なのでレガシーとまではいかなくとも、随分と枯れた技術なのかもしれません。

何はともあれ、そんなREST APIと、(NoSQLも大変魅力的ながら)やはりまだまだ引っ張りだこのRDBによるシステムをCDKで、構築作業などをなるべく手間かけずにサクッと行えるようにしてみました。参考になれば幸いです。

今回の内容は複数記事(3本程度を予定)に分けて公開する予定です。本記事ではシステム概要と実装前半部分(スタックそのものやVPCの実装)についてお伝えします。

この記事はこのような方におすすめ

  • ビジネスレベル(BtoBでもBtoCでも)で用いるREST APIシステムの構築方法を模索されている方
  • CI/CDパイプラインやIaCなどを導入して、運用負担も少ないシステムを構築したい方

システムの概要

今回はこのようなシステムを作成します。

API GatewayとLambdaで構築されたREST APIがあります。またCognitoとも接続して認証処理も行えるようにしました。ユーザーはブラウザを通して操作を行う訳ですが、その際にJSなどによってCognitoやAPI Gatewayと通信を行って、データの操作などが行われます。
REST APIはRDB(今回はAurora Serverless v2)にCRUD操作を行い、その結果がフロント側に返却されます。

以上がユーザーによる操作に関連した説明ですが、それとは別に管理者がDBの管理を行うため、踏み台インスタンスを設けています。

アーキテクチャ図にはありませんが、システム全体をCI/CDパイプラインを用いて手間なく更新できるようにする予定です。

シンプルな構成ですが、REST APIでよくあるビジネス要件は網羅できたかと思います。

ソースコード/構築手順など

https://github.com/aitp-tomo/demo-api

上記GitHubリポジトリにソースコードや構築手順などを記載しておきました。

構築手順などをお読みいただくとお分かりいただけるかと思いますが、最初に少しだけ手動での作業を行うだけで、後はCDKにより自動でシステムが構築されます。改修もCI/CDパイプラインがあるので手動での作業による手間、ミスをなくすことができます。

以下より、README.mdなどに記述しづらいリソースの詳細や注意点などを記述していきます。
予め断っておきますが、今回のシステムはブログ記事の題材としては比較的大きいこともあり、基礎的な説明などは省いていきますのでご了承ください🙏

実装

CDK Pipelines

実際のリソースについて触れていく前に、リソースを構築するためのパイプラインについて説明します。

CDKではデプロイを自動化するCDK Pipelinesが提供されています。CDK Pipelinesを利用すれば、CodeCommitやGitHub上のソースでリソースをビルド、デプロイできます。

今回はソース元に弊社でも用いているGitHubを採用しました。

そんなCDK Pipelinesでパイプラインを構築しているのは下記ソースになります。
https://github.com/aitp-tomo/demo-api/blob/main/lib/stack/CdkPipelineStack.ts
https://github.com/aitp-tomo/demo-api/blob/main/lib/stack/Stage.ts

// 一部抜粋
  private readonly initialize = (props: Props): void => {
    this.createPipeline();
    this.addStage(props);
  };

  private readonly createPipeline = (): void => {
    const pipelineId = `${this.appId}-pipeline`;
    this.pipeline = new pipelines.CodePipeline(this, pipelineId, {
      pipelineName: pipelineId,
      synth: new pipelines.ShellStep(`${pipelineId}-shell-step`, {
        input: pipelines.CodePipelineSource.connection(
          `${this.repoOwnerName}/${this.repoName}`,
          this.branchName,
          {
            connectionArn: `arn:aws:codestar-connections:${this.region}:${this.account}:connection/${this.connectionId}`,
          }
        ),
        commands: ["npm ci", "npm run build", "npm run test", "npx cdk synth"],
      }),
      dockerEnabledForSynth: true,
      dockerEnabledForSelfMutation: true,
    });
  };

  private readonly addStage = (props: Props): void => {
    const app = new Stage(this, `${this.appId}-stage`, props);
    this.pipeline.addStage(app);
  };

パイプライン自体を作成して、そこにリソースを構築するステージを追加します。

また、今回はLambda関数の構築でDockerがパイプラインで用いられる(Lambda関数をTypeScriptソースからコンパイルする際にDockerを用いるらしい)ので、パイプラインの定義にdockerEnabledForSynthdockerEnabledForSelfMutationを指定しましょう。
https://dev.classmethod.jp/articles/cdk-pipelines-codebuild-error/

メインスタック

上記パイプラインによってメインスタックが構築されます。
メインスタック自体のソースは下記になります。
https://github.com/aitp-tomo/demo-api/blob/main/lib/stack/MainStack.ts

こちらではAPI GatewayによるREST APIの構築と、別ソースを呼び出して各AWSリソースを構築しています。

VPC/Subnet/IGW etc…

ここからが実際にシステムとして稼働するAWSリソースの構築です。

まずはDBやLambda関数などが立ち上がるVPCなどの準備を行いましょう。主なソースは下記になります。
https://github.com/aitp-tomo/demo-api/blob/main/lib/wrapper/VpcWrapper.ts

なお、今回のソースではリージョンを東京リージョンに指定しています。もしも他のリージョンで試してみたい場合には下記ソースの記述も変更してください(他のリージョンでの動作確認は行っていないので、「他にも修正すべき箇所があった」などの場合には何卒ご容赦ください🙏)。

さて、VPCの他にもDBやLambda関数、踏み台インスタンスが立ち上がるサブネットを用意して、それぞれを繋げるためインバウンドルール、アウトバウンドルールを作成したりします。

ここで注意すべき点が2つほどあります。
第一に、Lambda関数の立ち上がるサブネットはプライベートサブネットにすること。特にVPC上のRDBと接続しながら、API Gatewayから呼び出されたり、AWSサービスを呼び出したりする場合には(まさに今回のケースですが)Lambda関数をパブリックサブネットに作るというイメージを持ちがちですが、パブリックサブネットだと動作しません。
https://qiita.com/kzmasa/items/c34adbf4c3ea78926ae0
なのでプライベートサブネットでLambda関数を立ち上げる必要があります。
API Gatewayからのプライベートサブネット上のLambda関数の呼び出しは特に何も設定せずとも可能ですが、パブリックインターネット上のAWSサービスをLambda関数から呼び出す際にはVPCエンドポイントが必要になります。今回はSecrets Manager上にあるDB接続情報を参照するため、Secrets ManagerとのVPC Endpointを作成しました。

第二に、セキュリティグループ同士のインバウンドルール/アウトバウンドルールを作成する際の注意点です。
例えばLambda関数用のセキュリティグループ(リクエストを送る側)からRDB用のセキュリティグループ(Lambda関数からリクエストを受け取る側)へのリクエストを許可するアウトバウンドルールを、RDB用のセキュリティグループにおいてLambda関数用のセキュリティグループからのリクエストを許可するインバウンドルールを作成する際、

lambdaSecurityGroup.addEgressRule(dbSecurityGroup, ec2.Port.tcp(3306));
dbSecurityGroup.addIngressRule(lambdaSecurityGroup, ec2.Port.tcp(3306));

とするとエラーが発生します。一見すると問題ないかのように思いますが、これだとそれぞれのセキュリティグループの定義に互いのセキュリティグループが参照される、循環参照にハマってしまいます。
https://qiita.com/theFirstPenguin/items/5f54d5f1b45e4f618ab9
上記記事はCloudFormationに関する記事ですが、CloudFormationのラッパーであるCDKでも同じ問題が発生します。
なのでインバウンドルール、アウトバウンドルールをセキュリティグループとは別に作成する必要があります。
インバウンドルール、アウトバウンドルールは残念ながらL2では作成できず、L1でしか作成できない(L1/L2についてはこちら参照)ので下記ライブラリを参照しながら各ルールを作成してください。

次回は実装後半編

次回では実装後半編として、API gatewayとCognitoの接続方法や踏み台インスタンスなどについて紹介します。
是非ご期待ください👍

追記

続きが公開されましたので、是非ご一読ください。

関連するタグ