オープンAPIの定義からAmazon API Gatewayを構築する

マネックス証券 システム管理部のHです。
以前はシステム開発推進部という部署におりましたが、この夏にシステム管理部に異動しました。

はじめに

今回もCDKを使ってAmazon API Gatewayを構築します。もちろん使用するプログラミング言語は今までと同じJavaです。ただし今までと作り方が変わります。
今まではAPI Gatewayのリソースやメソッドは、CDKのライブラリを使用してすべてJavaのロジックで作っていました。(ここに詳しく書いています)
今回APIの定義についてはオープンAPIを使用して、さらにAPI Gateway構築はCDKを使用して行います。
APIの定義とAWSへのデプロイを分けるということになるのですが、一般的なWeb API環境の設計や構築においては、以下のようなメリットがあると思います。

  • API定義については、業務アプリケーション開発担当者が業務を踏まえて設計します。業務やオープンAPIの知識は必要ですが、AWSに関する深い知識は不要です。(もちろんあるに越したことはありません)
  • AWSへのデプロイについては、AWSの構築担当者がAWSの知識・経験を駆使して構築します。業務について深い知識は不要です。(こちらも業務知識があった方がもちろん良いです)
  • API GatewayがオープンAPIの仕様に対応しているため、過去のエンジニアブログで書いたようなリソースやメソッドをひとつづつJavaのロジックで作成するような実装は必要ありません。

やりたいこと

このようなリソース1つ、GETメソッドのみのシンプルなAPI GatewayをCDKで構築します。ただ上記の通りリソース、メソッドについてはオープンAPIで定義してそれをCDKで取り込みます。

どうやって実現するか考える

CDKでAPI Gatewayを構築する方法として、前回RestApiを使用しましたが、今回はSpecRestApiを使用します。 SpecRestApiはAPI定義を読み込み、そこからAPI Gatewayを構築できます。ただし統合リクエストや統合レスポンスの設定などは、オープンAPIの仕様にあるわけではないので、工夫が必要です。

実装してみる

実装してみます。実装は以下の順番で行います。

  • API定義を作成する。
  • API定義ファイルを読み込む。
  • API定義を修正する。
  • CDKでAPI Gatewayを構築する。

API定義を作成する。

API定義は、上記の通りパス(API Gatewayにおけるリソース)1つ、GETメソッドのみでシンプルなものとなります。

openapi: 3.0.1
info:
  title: Monex Engineer Blog 2022/12
  description: Monex Engineer Blog 2022/12
  version: "1.0"
servers:
- url: http://localhost:8080
paths:
  /api:
    summary: test api
    get:
      summary: テストGETメソッド
      description: エンジニアブログ用テストGETメソッド
      operationId: ""
      responses:
        default:
          description: Default error sample response
          content:
            application/json:
              examples:
                test:
                  value: |-
                    {
                      "statusCode": 200,
                      "message": "OK"
                    }
        "200":
          description: OK response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/data'
components:
  schemas:
    data:
      required:
      - data
      type: object
      properties:
        statusCode:
          type: integer
          format: int32
          example: 200
        message:
          type: string
          example: Test

API定義ファイルを読み込む。

API定義ファイルを読み込むのには、Swagger Parserを使用します。
build.gradleでは依存関係として下記を定義します。

implementation "io.swagger.parser.v3:swagger-parser:2.1.9"

ファイルからAPI定義を読み込みます。ファイルの書き方が間違っている場合は、ここでエラーになります。(resultオブジェクトの中にエラーメッセージが入ります)

SwaggerParseResult result = new OpenAPIParser().readLocation("API定義ファイルパス", null, null);
OpenAPI openApi = result.getOpenAPI();

API定義を修正する。

これでAPI定義は読み込めたのですが、API Gatewayでは統合リクエストなどの情報が必要となるため、このままではAPI Gatewayとして機能しません。
そのため、読み込んだAPI定義に統合リクエストなどの情報を追加してあげる必要があります。
追加には、オープンAPIのベンダー拡張機能を使用します。AWSであれば、「x-amazon-apigateway」で始まるオブジェクトやプロパティをAPI定義に追加します。

ベンダー拡張機能については、下記に記載があります。統合リクエストや統合レスポンスの設定だけではなくリソースポリシーやLambdaオーソライザー、ゲートウェイレスポンスなどの設定も行えます。 docs.aws.amazon.com

今回は1パス1メソッドなので、、「x-amazon-apigateway」の設定をAPI定義ファイルに記載しても問題ないのですが、パスやメソッドが増えてくると1つ1つ追加していくのは面倒だし、可読性も落ちると思うので、プログラム内で追加することにします。統合リクエストは、今回モックにしているので固定レスポンスを返すようにしていますが、実際にはLambdaだったり、バックエンドへのリクエスト送信だったりすると思います。

private void addOtherInfo(OpenAPI api) {
    // パス単位でループ
    for (Entry<String, PathItem> entry : api.getPaths().entrySet()) {
        String endpoint = entry.getKey();
        PathItem path = entry.getValue();

        // 統合リクエストの処理
        Operation opeGet = path.getGet();
        Map<String, Object> ext = new LinkedHashMap<>();
        ext.put("passthroughBehavior", "when_no_match");
        ext.put("type", "mock");
        ext.put("httpMethod", "GET");
        ext.put("responses", Map.of(
            "default", ImmutableSortedMap.of(
                "statusCode", 200,
                "responseTemplates",
                    Map.of("application/json",
                            "{\r\n    \"statusCode\": 200,\r\n"
                                    + "    \"message\": \"Hello Japan!!!\"\r\n}"))));
        ext.put("requestTemplates", Map.of("application/json",
                "{\"statusCode\": 200}"));
        opeGet.addExtension("x-amazon-apigateway-integration", ext);
    }
}

修正したAPI定義を一度ファイルに保存します。
旧ファイル名と区別するために、新ファイル名にはデータのサイズを付与しています。

String yamlString = Yaml.pretty(openApi);
logger.debug("修正後のAPI定義:{}", yamlString);
String 新ファイル名 =
    旧ファイル名.replaceAll(".yaml", "."
        + yamlString.getBytes().length + ".yaml");

Files.writeString(Path.of("新ファイル名"), yamlString);

ちなみに修正後のAPI定義は以下のようになります。「x-amazon-apigateway-integration」の部分が追加されています。

openapi: 3.0.1
info:
  title: Monex Engineer Blog 2022/12
  description: Monex Engineer Blog 2022/12
  version: "1.0"
servers:
- url: http://localhost:8080
paths:
  /api:
    summary: test api
    get:
      summary: テストGETメソッド
      description: エンジニアブログ用テストGETメソッド
      operationId: ""
      responses:
        default:
          description: Default error sample response
          content:
            application/json:
              examples:
                test:
                  value: |-
                    {
                      "statusCode": 200,
                      "message": "OK"
                    }
        "200":
          description: OK response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/data'
      x-amazon-apigateway-integration:
        passthroughBehavior: when_no_match
        type: mock
        httpMethod: GET
        responses:
          default:
            responseTemplates:
              application/json: "{\r\n    \"statusCode\": 200,\r\n    \"message\"\
                : \"Hello Japan!!!\"\r\n}"
            statusCode: 200
        requestTemplates:
          application/json: "{\"statusCode\": 200}"
components:
  schemas:
    data:
      required:
      - data
      type: object
      properties:
        statusCode:
          type: integer
          format: int32
          example: 200
        message:
          type: string
          example: Test

CDKでAPI Gatewayを構築する。

やっとここでAPI Gatewayを構築します。読み込むAPI定義に今回ApiDefinition.fromAsset()でAPI定義ファイルを指定していますが、他にもS3バケットから読み込んだり、JavaのロジックでAPI定義を書いて、それを元に構築することができます。

SpecRestApi.Builder
    .create(this, "createApi")
    .restApiName("EblogApi202212")
    .apiDefinition(ApiDefinition.fromAsset("新ファイル名"))
    .endpointTypes(List.of(EndpointType.REGIONAL))
    .deploy(true)
    .retainDeployments(false)
    .policy(null)
    .build();

CDKコマンドにより、AWSにデプロイします。

cdk deploy

デプロイすると、以下のようにAPI Gatewayが構築されます。CDKで作成した統合リクエストや統合レスポンスも追加されています。

統合リクエストはこんな感じです。マッピングテンプレートでstatusCodeを設定しています。

統合レスポンスはこんな感じです。マッピングテンプレートでステータスコードが200の場合の固定レスポンスを設定しています。

確認してみる

ターミナルでの確認

構築したAPIに対し、curlコマンドでAPI呼び出しを行います。想定したレスポンスが返ってきていることが確認できます。

おわりに

SpecRestApiを使う利点としては、APIの定義と構築の担当を分けることができるといったことがあります。CDKのプログラムを汎用的に作れば、いろいろなAPI定義を簡単にAWSにデプロイすることもできます。その一方でSpecRestApiはRestApiに比べできることが少なく、API定義の中でベンダ拡張「x-amazon-apigateway-integration」を使用して実現しなければいけないことも多く、ナレッジも少ないと思っています。
ただうまく使えばとても便利だと思うので、活用できればと思います。