📀

OpenAPI × AWS CDK × APIGateway でRest APIを管理する

2022/09/04に公開

OpenAPI定義からAPIGatewayをいい感じに作成できたので、伝えたかった記録になっています。

どんな流れで 「OpenAPI ×CDK」 に辿り着いたのか、つらつら書いています。

長い記事になっていますが、本当に伝えたいのは最後のCDKの話だけです!

時間が勿体ない方は最後の方だけ読んでいただけたらと思います。

【想定している層】

  • APIGateway そこそこ分かる
  • CDK 聞いたことある
  • OpenAPI(Swagger) 聞いたことない

【私のレベル】

  • APIGateway 2年くらい使っているけど未だにCorsエラーとかで沼る。
  • CDK 1ヶ月前くらいから触り始めたけど、既に好き。CloudFormationに戻れない。
  • OpenAPI 『スキーマ駆動開発』、とかの記事をみて気になっていて、最近初めて実際に触った。

OpenAPIとは?

「Restful APIの定義を記述するための仕様」です。

分かりやすく言えば、Restful API用の Infrastructure as Code と表現して差し支えないかと思います。(インフラではないが)

YAMLやJSONで定義ファイルを作成すると、それに基づいたAPI仕様書をエクスポートすることができます。

さらにさらに、便利な周辺ツールが充実しており、モックサーバーを動かしたり・定義したAPIを使用するためのプログラムを生成出来たりします。

と、エラそうに解説してますが、2011年からあるものらしいです。

以前は Swagger の名称であり、いろいろあってOpenAPIに変わりました。

いまでも周辺ツール群には “Swagger” の名称が残っています。

OpenAPIの基本情報は、他のサイトを参考にしてください。(間違ったことを書きたくないので…)

拙い説明よりこちらなど見ていただけたら幸いです!

https://devblog.thebase.in/entry/2022/03/28/130016

OpenAPIの定義ファイルを見てみよう

実際の定義ファイルは以下のようになります。

今回の記事では内容まで詳しく説明しません。

openapi: 3.0.3
info:
  title: Hello API
  description: Defines an example “Hello World” API
  version: "0.0.1"
paths:
  "/":
    get:
      operationId: sayHello
      summary: Say Hello
      description: Prints out a greeting
      parameters:
      - name: name
        in: query
        required: false
        schema:
          type: string
          default: "World"
      responses:
        "200":
          description: Successful response
          content:
            "application/json":
              schema:
                $ref: "#/components/schemas/HelloResponse"
components:
  schemas:
    HelloResponse:
      description: Response body
      type: object
      properties:
        message:
          type: string
          description: Greeting
          example: Hello World!

API仕様書を生成してみよう

先ほどの定義ファイルから、ドキュメントを生成してみます。

ドキュメント生成には redoc [1]を使いました。

以下を実行します。

redoc-cli bundle openapi.yaml

こんな感じのHTMLが生成されます。

このように、API定義書をポチポチExcelやらスプレッドシートで作る手間が省けます。

ローカルでモックサーバーを動かしてみよう

定義ファイルをもとに、ローカルのモックサーバーを動かすことも可能です。

これにより、「バックエンドがまだできていないけど、フロントからAPI実行したい!」といったニーズを簡単に満たすことができます。

ローカルでモックサーバーを動かすには、Prism [2]というツールを利用します。

以下を実行します。

npx prism mock openapi.yaml -p 8080

すると、モックサーバーが起動します。

curlコマンドを叩いてみます。

こんな風に、モックサーバーを立ち上げることができました。

他にも、定義ファイルからそのAPIを利用するためのプログラミングコードを生成するツールなどもありますが、今回は割愛します。

とにかく、「OpenAPIは便利だ!」と伝われば良いです。

API GatewayにOpenAPI定義を反映してみる

さて、OpenAPIの説明を書いていたのは、この後の話をするためです。

実はAWS の API GatewayではOpen APIの定義ファイルを読み込んでリソースを作成する機能があります。

これを使ってみます。

ピュアなOpenAPI定義ファイルを読み込ませる

まず、ピュアなOpenAPI定義ファイルを読み込ませます。

今回はマネジメントコンソールから操作しました。

ポチポチしてインポートすると、APIのリソースが作成されました。

ただし、この状態だとAPIGatewayと紐づけるサービスの設定などを何もしていないので、使える状態ではありません。

このままでは意味が無いので、ピュアなOpenAPI定義を使うのはここまでになります。

APIGateway独自のOpenAPI拡張定義を利用する

OpenAPIの定義をAPIGatewayで使う際には、『APIGateway独自の拡張定義』を利用することができます。

https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-swagger-extensions.html

例えば「Lambda統合のAPIを作成したい!」となった場合、x-amazon-apigateway-integrationの設定値を利用します。

それでは、拡張定義を利用したOpenAPI定義を作成し、Lambda統合のAPIを作成してみましょう。

以下の手順で進めていきます。

  1. Lambda統合用のLambda関数を作成
  2. OpenAPI定義を修正し、統合するLambda関数に 1 で作成したものを指定
  3. APIGatewayにIAM権限が足りないので設定する

1で適当にLambda関数を作成します。(マネジメントコンソールで作成しました。ここは割愛)

2で修正したOpenAPI定義は以下のようになります。

<Function Arn>に 1 で作成した関数のARNを入力します。

openapi: 3.0.3
info:
 title: Hello API
 description: Defines an example “Hello World” API
 version: "0.0.1"
paths:
 "/":
   get:
     operationId: sayHello
     summary: Say Hello
     description: Prints out a greeting
     parameters:
     - name: name
       in: query
       required: false
       schema:
         type: string
         default: "World"
     responses:
       "200":
         description: Successful response
         content:
           "application/json":
             schema:
               $ref: "#/components/schemas/HelloResponse"
+      x-amazon-apigateway-integration:
+        httpMethod: "POST"
+        uri: "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/<Function Arn>/invocations"
+        passthroughBehavior: "when_no_match"
+        type: "aws_proxy"
 components:
   schemas:
     HelloResponse:
       description: Response body
       type: object
       properties:
         message:
           type: string
           description: Greeting
           example: Hello World!

こちらを再度APIGatewayでインポートすると、以下のようにLambda統合の設定が完了しました。

しかし、こちらをテスト実行すると以下のエラーが出ます。

Execution failed due to configuration error: Invalid permissions on Lambda function

APIGatewayにIAMロールが割り当てられていないからです。

というわけで、Lambda実行用のロールを付けて再実行します。

まず、適切なIAMロールを作成します。(こちらは説明を割愛)

作成したIAMロールをOpenAPI定義を修正して指定します。

openapi: 3.0.3
 info:
   title: Hello API
   description: Defines an example “Hello World” API
   version: "0.0.1"
 paths:
   "/":
     get:
       operationId: sayHello
       summary: Say Hello
       description: Prints out a greeting
       parameters:
       - name: name
         in: query
         required: false
         schema:
           type: string
           default: "World"
       responses:
         "200":
           description: Successful response
           content:
             "application/json":
               schema:
                 $ref: "#/components/schemas/HelloResponse"
       x-amazon-apigateway-integration:
         httpMethod: "POST"
         uri: "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/<Function Arn>/invocations"
         passthroughBehavior: "when_no_match"
         type: "aws_proxy"
+        credentials : "<IAM Role Arn>"
 components:
   schemas:
     HelloResponse:
       description: Response body
       type: object
       properties:
         message:
           type: string
           description: Greeting
           example: Hello World!

これでテスト実行をすると、ようやく成功させることができました。

ここまで、しっかり読んでいただいた方も、流し読みした方も、試しているときの私も、こう思ったはずです。

「めんどくせえ!!」

OpenAPI定義からAPIGatewayのリソースを作成するのは、個人的にイケてないと思いました。

主な理由は3つです。

  1. OpenAPI定義にAWS用の設定値を記載するのが嫌だ
  2. AWS用の拡張定義を覚える時間が無駄だ
  3. そもそも今はCloudFormationなどのIaCでAPIGatewayも定義することが多いので、使うことがない

1について

OpenAPI定義のほうはドキュメント生成やローカルでのモックサーバー起動を主な役割としたいです。そこにAWSのAPIGatewayの設定やIAMの設定が紛れ込ませたくありません。

紛れ込むと、「OpenAPIの記法は知っているがAPIGateway用の拡張記法は知らない人」にとっては、読みづらく煩雑な定義ファイルとなってしまいます。

2について

わざわざ「APIGatewayに反映する」という1つの目的のために拡張定義の設定値を覚える・調べる手間が勿体ないと感じます。

3について

CloudFormation・SAM・Terraformといった IaC でAWSリソースを管理することが多く、「OpenAPI定義からAPIGatewayリソースを作成したい!」というニーズ自体が少ないだろうと感じました。

こんな感じに「つらみ」がありました。

これらの「つらみ」を解消するには、以下が出来れば良さそうです。

  • OpenAPI定義はピュアなままで維持する
  • APIGatewayなどAWSのリソースはIaCで管理する
  • OpenAPI定義をAPIGatewayに上手いこと反映する

というわけで、AWS CDKを使った方法を見ていきます。

実はここからが本題です。

OpenAPI × Infrastructure as Code

CDKの話に入る前に、Cloud FomrationでOpenAPI定義を利用できないか確認してみます。

OpenAPI × CloudFormation

私は実際に試していないですが、AWS::Serverless::Api のリソースを利用し、 DefinitionBody の値にOpenAPIの定義ファイルを指定することが出来るようです。(厳密には SAM のリソースです)

しかし、この場合もOpenAPIの定義側にLambda統合の設定値(x-amazon-apigateway-integration)などを書く必要が出てくるようです。

こちらの記事[3]を参考にさせていただきました。

この状態だと、先ほど言った以下が実現できません。

  • OpenAPI定義はピュアなままで維持する
  • APIGatewayなどAWSのリソースはIaCで管理する

『OpenAPI × CloudFormation』はここで終わりにします。

OpenAPI × AWS CDK(標準)

AWS CDKとは、『Cloud Development Kit』の略です[4]

AWSのインフラストラクチャをコード(TypeScript, Go, Python など)で管理できる優れものです。

現在 CDK for Terraform[5]というものも登場しており、アツい話題だと思っています。

OpenAPI と AWS CDKの組み合わせは上手く行きそうなので、紹介させていただきます。

まず、CDKの標準の機能としてOpen API定義をAPIGatewayに反映する方法はありました。

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway-readme.html#openapi-definition

ただし、こちらの機能でも先ほどの Cloud Formation と同様に、OpenAPIの定義側にAPIGatewayの設定値を記載する必要が出てきます

OpenAPIの定義から作成したAPIGatewayのリソースは、CDKでの管理が出来なくなってしまいます。

そのため、標準の機能では解決することが出来ません。

OpenAPI × AWS CDK(拡張)

このあたりのつらみを同じように感じている方がおり、解決するためのライブラリが既に開発されていました。

**『CDK Day』**というイベントで紹介されています。

YouTubeのvideoIDが不正ですhttps://youtu.be/Ey7bNVT4W1g?t=12810

そして、こちらのライブラリを利用すると、OpenAPIの定義はピュアなままに保ちつつ、CDKのコード側でAWSの設定値を追加することが可能になります。

https://github.com/alma-cdk/openapix

npm i -D @alma-cdk/openapix

先ほど記載していたピュアなOpenAPIの定義とこちらのライブラリを使って、CDKでデプロイするための方法を見ていきましょう。

まず、先ほどのピュアなOpenAPI定義はこちらです。

x-amazon-apigateway-integration の記載を削除しました。

openapi: 3.0.3
 info:
   title: Hello API
   description: Defines an example “Hello World” API
   version: "0.0.1"
 paths:
   "/":
     get:
       operationId: sayHello
       summary: Say Hello
       description: Prints out a greeting
       parameters:
       - name: name
         in: query
         required: false
         schema:
           type: string
           default: "World"
       responses:
         "200":
           description: Successful response
           content:
             "application/json":
               schema:
                 $ref: "#/components/schemas/HelloResponse"
 components:
   schemas:
     HelloResponse:
       description: Response body
       type: object
       properties:
         message:
           type: string
           description: Greeting
           example: Hello World!

そして、CDKのコードを書いていきます。

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Runtime } from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import * as openapix from '@alma-cdk/openapix';
import * as path from 'path';

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

    const greetFn = new NodejsFunction(this, "greet", {
      entry: path.join(__dirname, '../lambda/hello-function.ts'),
      runtime: Runtime.NODEJS_16_X,
      handler: 'handler',
    });

    new openapix.Api(this, 'HelloApi', {
      source: path.join(__dirname, '../openapi/openapi.yaml'),
      paths: {
        '/': {
          get: new openapix.LambdaIntegration(this, greetFn),
        },
      },
    })
  }
}

注目すべきは、OpenAPI定義から作成したAPI Gatewayのリソースに、Lambda統合の設定をCDK側のコードで制御できている点です。

このようにすることで、ピュアなOpenAPIの定義は保ったまま、CDKのコード側でAWSの設定を行うことが出来ます。

というわけで、ライブラリのおかげ様で以下が実現できました。

  • OpenAPI定義はピュアなままで維持する
  • APIGatewayなどAWSのリソースはIaCで管理する
  • OpenAPI定義をAPIGatewayに上手いこと反映する

こちらのライブラリがどれほどの完成度なのか、使い込まないことには分からないですが、使いこんでバグが出たらプルリクを投げたり、自分で修正したりして行きたいです。

また、これ以外にも自分でCDKを拡張して今回のライブラリと同じような機能を作ることも出来るはずです。

私はまだ、そこまでのレベルに達していませんが、実務で使う際には自分で拡張するところまで視野に入れていきたいです。

まとめ

  • OpenAPI × APIGateway のみでは「つらみ」が多い
  • CDKと @alma-cdk/openapix のライブラリを使うことで、OpenAPIの定義を上手いことAPIGatewayに反映できる
  • バグが出ないとは限らないので、実務で使う際には注意が必要

感想

本当は最後のライブラリとCDKを使うと簡単に出来る、ということを書きたかったのですが、長く書いちゃいました。

今回の作業をしている際に、自分が「このポイントがイケてない」と思ったことが、世の中的に同様の認識されていると分かったのが嬉しかったです。

自分で作業しているときに、この記事の流れと同じような順番で最後の解決に辿り着いたので「この流れを誰かにみてほしい!!」という思いもあります。

本当にCDKはお気に入りなので、今後も試していきたいです!!

脚注
  1. https://redocly.github.io/redoc/ ↩︎

  2. https://github.com/stoplightio/prism ↩︎

  3. https://dev.classmethod.jp/articles/serverless-swagger-apigateway/ ↩︎

  4. https://aws.amazon.com/jp/cdk/ ↩︎

  5. https://www.terraform.io/cdktf ↩︎

Discussion