AWS Amplify で OpenAPI 定義をインポートして REST API をデプロイする

# 概要
AWS Amplify では11月のアップデートで追加された「オーバーライド」機能により、バックエンドリソースを AWS CDK でカスタマイズできるようになりました :tada:

https://aws.amazon.com/jp/blogs/news/override-amplify-generated-backend-resources-using-cdk/

[2021/11/23 リリースの v7.5.0](https://github.com/aws-amplify/amplify-cli/releases/tag/v7.5.0) から API カテゴリーでも利用可能となっていて、この新機能を利用して今までできなかった OpenAPI 定義ファイルを利用した API Gateway (REST) リソースのデプロイをしてみたいと思います。


:::note warn
注意：
執筆時点（2021/12/13 時点）ではこのオーバーライド機能でできることが一部制限されており、ワークアラウンドで対応した部分を含んでいます（詳細は後述します）。今後修正加筆される可能性があることをご了承ください。
:::

# やってみる
## 構成
AWS Amplify で以下のようなシンプルな OpenAPI 定義 (OAS3) をもとに API Gateway + Lambda で構成された REST API のバックエンドを作成してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233097/4ab60a62-53bc-f43f-d764-1933d2e45a91.png)


## 環境
執筆時は AWS Cloud9 上で Amplify CLI v7.6.3 をインストールして実施しました。

## 手順
### 1. Amplify CLI のセットアップとプロジェクトの初期化

Amplify CLI をまだインストールしていない場合は、公式ドキュメントの記載を参考にインストールし、その後 Amplify CLI のセットアップを行います。

```bash
npm install -g @aws-amplify/cli
```

https://docs.amplify.aws/cli/start/install/

次に、公式ドキュメントの「Initialize new project」の記載にしたがってプロジェクトを初期化しましょう（詳細な手順は割愛します）。

```bash
amplify init 
```

https://docs.amplify.aws/cli/start/workflows/

今回は `ap-northeast-1` リージョンに `OpenAPISample` というプロジェクト名で作成してみました。


### 2. Amplify CLI での API カテゴリの追加とデプロイ

Amplify プロジェクトを初期化したら、以下のコマンドから Lambda 関数とともに REST API のバックエンドを追加していきます。

```bash
amplify add api
```

今回は以下のように回答を選択しました。

- Select from one of the below mentioned services: `REST`
- Provide a friendly name for your resource to be used as a label for this category in the project: `Greeting`
- Provide a path (e.g., /book/{isbn}): `/hello`
- Only one option for [Choose a Lambda source]. Selecting [`Create a new Lambda function`].
- Provide an AWS Lambda function name: `Hello`
- Choose the runtime that you want to use: `NodeJS`
- Choose the function template that you want to use: `Hello World`
- Do you want to configure advanced settings? `No`
- Do you want to edit the local lambda function now? `No`
- Restrict API access? (Y/n) `no`
- Do you want to add another path? (y/N) `no`


### 3. REST API のデプロイ（初回）

初期設定のまま API をデプロイしてみます。

```bash
amplify push -y
```

しばらく待って `All resources are updated in the cloud` と表示されたら、マネジメントコンソールから「Greeting」という名前の API Gateway リソースができていることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233097/16004a6a-149a-c214-a719-d30bee070f96.png)

また、「Hello-dev」という名前の Lambda 関数も作成されていることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233097/b4bf0cdd-5ccb-a38a-233e-609a09835b40.png)


ここまでで、テンプレートをもとに `/hello` というパスへのリクエストに対して Lambda から `"Hello from Lambda!"` というレスポンスを返す API がデプロイされました。

curl コマンドで以下のようなレスポンスとなることを確認しましょう。

```bash
$ curl https://**********.execute-api.ap-northeast-1.amazonaws.com/dev/hello
"Hello from Lambda!"
```

（補足）： `**********` の部分にはデプロイされた API Gateway のリソース ID を入力します


### 4. OpenAPI 定義ファイルの作成と配置

手順 3. でデプロイした API を OpenAPI 定義ファイルの設定をもとに更新していきます。
以下のような OpenAPI 定義ファイルを `openapi.json` という名前で作成し、プロジェクトディレクトリ配下の `amplify/backend/api/Greeting` に配置します。

```json:openapi.json
{
  "openapi" : "3.0.1",
  "info" : {
    "title" : "Greeting",
    "version" : "1.0.0"
  },
  "paths" : {
    "/hello" : {
      "get" : {
        "parameters" : [ {
          "name" : "name",
          "in" : "query",
          "required" : true,
          "schema" : {
            "type" : "string"
          }
        } ],
        "responses" : {
          "200" : {
            "description" : "200 response",
            "content" : {
              "application/json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Response"
                }
              }
            }
          }
        },
        "x-amazon-apigateway-request-validator" : "クエリ文字列パラメータおよびヘッダーの検証",
        "x-amazon-apigateway-integration" : {
          "httpMethod" : "POST",
          "uri" : "<PLACEHOLDER>",
          "responses" : {
            "default" : {
              "statusCode" : "200"
            }
          },
          "passthroughBehavior" : "when_no_match",
          "contentHandling" : "CONVERT_TO_TEXT",
          "type" : "aws_proxy"
        }
      }
    }
  },
  "components" : {
    "schemas" : {
      "Response" : {
        "required" : [ "message" ],
        "type" : "object",
        "properties" : {
          "message" : {
            "type" : "string"
          }
        }
      }
    }
  },
  "x-amazon-apigateway-request-validators" : {
    "クエリ文字列パラメータおよびヘッダーの検証" : {
      "validateRequestParameters" : true,
      "validateRequestBody" : false
    }
  }
}
```

#### 解説

ここでは「Greeting」API を編集し、`/hello` パスの `GET` メソッドに対するリクエストを、手順 3. で作成した「Hello」という Lambda 関数に渡すように定義しています。

`x-amazon-` から始まるフィールドは Amazon 独自の設定項目です。

このうち `x-amazon-apigateway-integration` というフィールドでは Lambda 関数のプロキシ統合の設定をしています。`<PLACEHOLDER>` という箇所は、後で Amplify でデプロイされた Lambda 関数の情報で置換します。

また `x-amazon-apigateway-request-validator` というフィールドで、API Gateway の機能を使って `name` というクエリパラメータが存在するかの検証をする設定をしています。


### 5. オーバーライド機能の有効化と OpenAPI 定義ファイルの適用

手順 3. でデプロイした API に対して、オーバーライド機能を使って OpenAPI 定義ファイルをインポートしていきます。

まず、以下のコマンドからオーバーライド機能を有効化します。

```bash
amplify override api
```

回答には以下のように選択します。

- Do you want to edit override.ts file now? (Y/n) `yes`
- Choose your default editor: `None` => 好きなものを選択

すると、以下のようにプロジェクトディレクトリ配下の `amplify/backend/api/Greeting` に `override.ts` というファイルが作成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233097/e517a724-4268-905b-dd37-3aa4b528fa0a.png)


このファイルを編集することで API Gateway リソースの定義の編集ができるようになります。（詳細は以下のリンク先を参照）

https://docs.amplify.aws/cli/restapi/override/

`override.ts` を以下のように編集します。

```typescript:override.ts
// This file is used to override the REST API resources configuration
import { AmplifyApiRestResourceStackTemplate } from '@aws-amplify/cli-extensibility-helper';

export function override(resources: AmplifyApiRestResourceStackTemplate) {
  // (1) OpenAPI 定義をファイルから読込
  const openApiJson = require('../openapi.json');

  // (2) amplify-meta.json から Lambda 関数の情報を取得
  const amplifyMetaJson = require('../../../amplify-meta.json');
  const helloFunctionRegion = amplifyMetaJson.function.Hello.output.Region;
  const helloFunctionArn = amplifyMetaJson.function.Hello.output.Arn;

  // (3) OpenAPI 定義の環境に応じて変わる部分を編集
  openApiJson.paths['/hello']['get']['x-amazon-apigateway-integration']['uri'] = `arn:aws:apigateway:${helloFunctionRegion}:lambda:path/2015-03-31/functions/${helloFunctionArn}/invocations`

  // (4) 編集した OpenAPI 定義を resources.restApi の body プロパティに代入
  resources.restApi.body = openApiJson;

  // (5) API のデプロイメント更新のための CloudFormation リソースの変更
  resources.deploymentResource.overrideLogicalId('DeploymentAPIGW' + openApiJson.info.title + Date.now());
}
```

#### 解説

##### (1) 配置した OpenAPI 定義をファイルから読込

OpenAPI 定義ファイルを JSON 形式で作成し `require` コマンドで読み込んでいます（注意点参照）。`override.ts` はビルド時にトランスパイルされ、`override.js` という名前でプロジェクトディレクトリ配下の `amplify/backend/api/Greeting/build` に配置されますが、このロケーションからの相対パスを指定するようにしています。

:::note warn
注意点：
現状 override.ts 内に記載した処理は vm2 モジュールで提供される sandbox 内で実行されますが、この sandbox では現状 fs, util, os といった Node.js の built-in モジュールが利用できません。これは以下の Issue にも記載があります。
:::

https://github.com/aws-amplify/amplify-cli/issues/9063

:::note warn
OpenAPI 定義をファイルから読み込む場合、本来は swagger-parser などの YAML ファイルにも対応した OpenAPI 定義のパースに特化したツールを使うのがよいのですが、このツールでは util などの前述の built-in モジュールを利用する必要があるため、現状では利用できずこのようなワークアラウンドの方法をとっています。
:::

参考：swagger-parser

https://github.com/APIDevTools/swagger-parser


##### (2) amplify-meta.json から Lambda 関数の情報を取得

デプロイした「Hello」という名前の Lambda 関数の情報を `amplify/backend/amplify-meta.json` から読み込んでいます。

:::note warn
注意点：
Amplify にはプロジェクトのバックエンドリソースを CDK 向けにエクスポートする機能がある（詳細は以下のリンク先を参照）ため、こちらを活用するのがよいと思われますが、前の話と同様、現状 fs, util, os といった Node.js の built-in モジュールが利用できないためその方法がとれず、ワークアラウンドでこのように対応しています。
:::

https://docs.amplify.aws/cli/usage/export-to-cdk/

:::note warn
ちなみに amplify-meta.json の中身は AWS 上にバックエンドがデプロイされるまで ARN などの値は参照できないため、例えば Lambda が未デプロイの場合は amplify push function を実行して先にデプロイしておく必要があることに注意しましょう。
:::


##### (3) OpenAPI 定義の環境に応じて変わる部分を編集

OpenAPI 定義の `x-amazon-apigateway-integration` というフィールドでは Lambda 関数の URI を指定しています（詳細は以下のリンク先を参照）。

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

ここは Amplify の環境に応じて動的に変わる部分のため、OpenAPI 定義をファイルから読み込んだ後の JSON オブジェクトに対して上書きしています。他にも環境に応じて変わるような項目があれば、同じように上書きしておきます。


##### (4) 編集した OpenAPI 定義を resources.restApi の body プロパティに代入

`override.ts` の override 関数の引数にあたる `resources` の中身を編集します。`resources.restApi` には 以下リンク先の `AWS::ApiGateway::RestApi` の設定が入っており、この `body` プロパティに代入することで OpenAPI 定義を API に適用できます。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-restapi.html


##### (5) API のデプロイメント更新のための CloudFormation リソースの変更

APIのリソースの定義の修正は Deployment を更新することで反映されますが、CloudFormation の制約により `AWS::ApiGateway::RestApi` の設定を変更しただけでは Deployment は更新されません。
以下の Issue の記載にしたがい、API Gateway の Deployment の CloudFormation リソース名 (Logical ID) を変更しておきます。すると `AWS::ApiGateway::RestApi` と同時にデプロイされるようになります。

https://github.com/aws-amplify/amplify-cli/issues/5894

今回は CloudFormation リソース名 (Logical ID) の末尾にタイムスタンプをつける対応をし、これにより `amplify push` をする度に毎回 Deployment がデプロイされるようにしていますが、これを OpenAPI 定義の中のバージョンなどと紐づけることで、更新タイミングをバージョン更新時のタイミングに制御することもできます。


### 6. Lambda 関数の編集

OpenAPI の定義に合わせてプロジェクトディレクトリ配下の `amplify/backend/function/Hello/src` のパスにある Lambda 関数を以下のように編集します。

```javascript:index.js
exports.handler = async (event) => {
    const name = event.queryStringParameters.name;
    const response = {
        statusCode: 200,
        body: JSON.stringify({message: `Hello, ${name}!`}),
    };
    return response;
};
```

クエリパラメータの `name` の値が出力メッセージに反映されるように変更しています。


### 7. REST API のデプロイ（オーバーライド有効化後）

上記で変更した API Gateway および Lambda 関数をデプロイします。

```bash
amplify push -y
```

しばらく待って `All resources are updated in the cloud` と表示されたら、マネジメントコンソールから「Greeting」という名前の API に変更が反映されていることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233097/cbae6e10-979d-7514-5691-4ad74c454b96.png)


また、「Hello-dev」という名前の Lambda 関数にも変更が反映されていることも確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233097/d066213e-f7ec-88c4-6e16-ab44bdf8a098.png)


curl コマンドで以下のように `name` というクエリパラメータを付与した場合に、以下のように出力メッセージにそれが反映されることを確認しましょう。

```bash
$ curl https://**********.execute-api.ap-northeast-1.amazonaws.com/dev/hello?name=hogehoge
{"message":"Hello, hogehoge!"}
```

（補足）： `**********` の部分にはデプロイされた API Gateway のリソース ID を入力します

一方で、クエリパラメータに `name` パラメータを付与せずに実行した場合の結果が以下のようになることを確認します。

```bash
$ curl https://xjuilfsqmb.execute-api.ap-northeast-1.amazonaws.com/dev/hello
{"message": "Missing required request parameters: [name]"}
```

OpenAPI 定義の中で API Gateway の機能によりクエリパラメータの検証を行う設定が正しく反映されています。


# まとめ

AWS Amplify のオーバーライド機能を使うことで、これまでできなかった OpenAPI 定義ファイルをベースにした REST API のデプロイができることが確認できました :raised_hands:

API 仕様書をベースにするスキーマファーストな開発を採用するケースが増えている昨今ですので、ぜひ Amplify による REST API 開発でもこの手法を活用していきましょう。

今回いくつかワークアラウンドで対応した部分もありますが、今後 Issue への対応が行われ、さらに使い勝手がよくなっていくことを期待しましょう。

それではまた！