AWS APIGateway × OpenAPI (1. 動機編)

2022-07-18 (2022-07-26 更新)

thumbnail

OpenAPI定義をCDKのREST API定義に統合するライブラリを開発しています。 これはライブラリの開発過程を紹介するシリーズの最初のブログ投稿です。

動機

最近、Cloud Development Kit (CDK)で記述したAmazon API Gateway (API Gateway)上のREST APIについてOpenAPI定義を書くべきだろうなと強く感じています。 私の知る限り、API GatewayのREST APIでOpenAPI定義を扱うオプションは2つあります。

  1. 既存のREST APIからOpenAPI定義をエクスポートする
  2. 既存のOpenAPI定義をインポートしてREST APIを作成する

1. 既存のREST APIからOpenAPI定義をエクスポートする

API Gatewayでは既存のREST APIからOpenAPI定義をエクスポートすることができます。 意味のあるOpenAPI定義をエクスポートするにはREST APIに別途ドキュメントを追加しなければなりません。 CDKにはL1 ConstructのCfnDocumentationPartがあり、それでAPI要素のドキュメントを書くことができます(「OpenAPIとREST APIを一緒に」の例を参照ください)。 しかし、ドキュメントと実際のAPI要素定義が分離しているのはドキュメントを最新に保つのに不都合かもしれません。 REST API要素とそのドキュメントを一緒に書くことができたら素晴らしいのではないかと考えています。

2. 既存のOpenAPI定義をインポートしてREST APIを作成する

AWS特有の拡張を含むOpenAPI定義が既に手元にあるなら、それをAPI GatewayでインポートしてREST APIを構築することができます。 CDKには既存のOpenAPI定義をインポートするための専用のConstruct SpecRestApiもあります。 しかし一からREST APIを構築するなら、私は以下の理由からCDKのパーツ(RestApiとサブ要素)を選びます。

  • 単純に慣れている。
  • CDKのパーツは面倒なIAM設定と脆弱性を減らすことができる。
  • OpenAPI定義にAWS特有の拡張を書くのは面倒かもしれない。*
  • 素のOpenAPI定義には多くの繰り返しが必要かもしれない。*

* このオプションは試していないので私が間違っているかもしれません。

3番目のオプション

ということで、CDKのパーツを使ってREST APIとOpenAPI定義を一緒に書くことができるような3番目のオプションがあるといいなと考えています。

デザインゴール

こちらのブログ投稿[1]は類似の成果を紹介しており刺激になりました。 しかし私の目的はTypeScriptの力を使いこなすことではありません。 目的は2つです。

  1. OpenAPIとREST APIを一緒に
  2. RestApiとの互換性

OpenAPIとREST APIを一緒に

本ライブラリを使用すると、OpenAPI定義を別のドキュメントリソースではなくREST API要素のすぐそばに記述することができます。

例えば、以下のようにすることができます。

const pet = api.root.addResource('pet');
const findByStatus = pet.addResource('findByStatus');
findByStatus.addMethod(
  'GET',
  new MockIntegration({
    // ... integration settings
  }),
  {
    operationName: 'findPetsByStatus',
    summary: 'Finds Pets by status',
    description: 'Multiple status values can be provided with comma separated strings',
    requestParameters: {
      'method.request.querystring.status': {
        description: 'Status values that need to be considered to filter',
        required: false,
        explode: true,
        schema: {
          type: 'string',
          enum: ['available', 'pending', 'sold'],
          default: 'available'
        }
      }
    },
    methodResponses: [
      {
        statusCode: '200',
        description: 'successful operation',
        responseModel: {
          'application/json': petArrayModel
        }
      }
    ]
  }
);

本ライブラリを使わない場合は以下のようになります。

const pet = api.root.addResource('pet');
const findByStatus = pet.addResource('findByStatus');
findByStatus.addMethod(
  'GET',
  new MockIntegration({
    // ... integration settings
  }),
  {
    operationName: 'findPetsByStatus',
    requestParameters: {
      'method.request.querystring.status': true
    },
    methodResponses: [
      {
        statusCode: '200',
        responseModel: {
          'application/json': petArrayModel
        }
      }
    ]
  }
);
new CfnDocumentationPart(this, 'FindPetsByStatusDocPart', {
  location: {
    type: 'METHOD',
    path: '/pet/findByStatus',
    method: 'GET'
  },
  properties: {
    summary: 'Finds Pets by status',
    description: 'Multiple status values can be provided with comma separated strings'
  },
  restApiId: api.restApiId
});
new CfnDocumentationPart(this, 'FindPetsByStatusParamsDocPart', {
  location: {
    type: 'QUERY_PARAMETER',
    path: '/pet/findByStatus',
    method: 'GET',
    name: 'status'
  },
  properties: {
    description: 'Status values that need to be considered to filter',
    required: false,
    explode: true,
    schema: {
        type: 'string',
        enum: ['available', 'pending', 'sold'],
        default: 'available'
    }
  },
  restApiId: api.restApiId
});
new CfnDocumentationPart(this, 'FindPetsByStatus200ResponseDocPart', {
  location: {
    type: 'RESPONSE',
    path: '/pet/findByStatus',
    method: 'GET',
    statusCode: '200'
  },
  properties: {
    description: 'successful operation'
  }
});

RestApiとの互換性

本ライブラリを使うと、RestApiを使うのと同じ使用感を得られます。

拡張されたConstruct RestApiWithSpecRestApiの代わりにインスタンス化するだけです。

const api = new RestApiWithSpec(this, 'example-api', {
  description: 'Example of RestApiWithSpec',
  openApiInfo: {
    version: '1.0.0'
  },
  openApiOutputPath: 'openapi.json',
  deploy: true,
  deployOptions: {
    stageName: 'staging',
    description: 'Default stage'
  }
});

RestApiWithSpecのインスタンスを得たあとは、RestApiと全く同様に使用することができます。 もちろんOpenAPI定義を記述するための拡張機能も提供されます(「OpenAPIとREST APIを一緒に」の例を参照ください)。

課題

いくつか課題があります。

  • RestApiとの互換性はどうやって実現するか?
    • RestApiのサブクラスやラッパーを書くとしたら、RestApiがサブ要素(ResourceMethod)としてインスタンス化するものを直接操作することはできません。 どうやってサブ要素を拡張したら良いでしょうか?
    • RestApiを再利用しないとしたら、互換性を実現するのは本当に大変なことになるでしょう。
  • いつ実際にOpenAPI定義ファイルを出力すべきか?
    • 保存のための関数をユーザーが明示的に呼び出さなければならないのでしょうか?
    • それともCDKがCloudFormationテンプレートに対してやるようにOpenAPIの定義ファイルもいつのまにやら保存されようにできるでしょうか?
  • Lambdaプロキシをサポートできるか?

今後のブログ投稿でこれらの課題に挑んでいきます。

まとめ

私が開発中のものはGitHubレポジトリで手に入ります。 次のブログ投稿ではRestApiとの互換性をどう実現するかに挑むつもりです。

参照

  1. OpenAPI Specs from CDK Stack WITHOUT Deploying First