OpenAPIのYAML分割管理と構成案

# はじめに

READYFORのエンジニアリング部に所属している熊谷です。

この記事は[READYFOR Advent Calendar 2020](https://qiita.com/advent-calendar/2020/readyfor)の8日目の記事です。

# 概要

**スキーマー駆動開発でOpenAPI（旧Swagger）を導入**し始めたところなのですが、その中で、OpenAPIの運用管理について色々調査・検討していたので、記事として共有させていただきます。

# 対象読者

以下の方々を対象としています

- API開発でOpenAPI導入を検討している方。
- 既にOpenAPIの導入済みの方。

# 背景 ( 課題感 )

スキーマー駆動開発でOpenAPI（旧Swagger）を採用している企業は多いかと思いますが、OpenAPI導入において最初に感じた課題感として、陥りそうな状況の一つとして、最初に運用方針を決めないまま、多数のメンバーが一つのopenapi.yamlにスキーマー定義を追加・更新していった場合、

1. ファイルサイズが膨れあがり、
2. スキーマー定義に一貫性がなくなり、
3. 見通しも悪化し、収拾つかなくなる。

みたいなケースが想定されるのではと思いました。

そのため、予めそのようなサービス拡大にも耐えられるように、また、マイクロサービスなど複数サービスにも対応できるように、OpenAPIの運用方針・構成を考えみました。（一部、実際に運用開始しています）

# OpenAPIの構成

OpenAPI専用のGitリポジトリを作成し、下記のような構成で構築します。
( 記事の最後にサンプルgitのリンクを貼っています )

## 構成イメージ

全体のディレクトリ構成のイメージです。

openapi.yamlは、直接編集するのではなく、openapi-generatorを使って中間ファイルから生成するようにします。中間ファイルを用いることでYAMLを分割して定義することができるようになります。

![Screen Shot 2020-12-14 at 8.42.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18841/736a2756-b63e-21db-e696-b7391727da23.png)


## ディレクトリ構成

具体的には、下記のようなディレクトリ構成になります。

```
./
├── README.md
├── openapi
│   ├── {サービス名}
│   │   └── openapi.yaml
│   └── api(ex)
│       └── openapi.yaml
└── src
    ├── components：全体の共通コンポーネント
    └── services：
        └── {サービス名}
            ├── root.yaml：中間ファイル
            ├── paths：各エンドポイントのスキーマー定義
            └── examples：Example用YAML
        ├── api(ex)
            ├── root.yaml：中間ファイル
            ├── paths：各エンドポイントのスキーマー定義
            │   ├── animals
            │   │   ├── cats.yaml
            │   │   └── dogs.yaml
            │   └── fruits
            │       └── apples.yaml
            ├── examples：Example用YAML
            │   ├── animals-cats-example-1.yaml
            │   ├── animals-dogs-example-1.yaml
            │   └── fruits-apples-example-1.yaml
├── scripts：各種生成スクリプト群
    ├── openapi2generator-ruby.sh
    ├── root2openapi.sh
    └── swagger-ui.sh
```


## ディレクトリ概要

各ディレクトリの概要と、そこに配置するYAMLファイル名のフォーマットです。

| ディレクトリ名 | 概要 | YAMLファイル名 | 
| --- | --- | --- |
| **openapi** | **OpenAPIファイル群**<br>・中間ファイルから自動生成されたファイル群<br>・直接このファイルは修正することはない |openapi.{サービス名}.yaml | 
| **src/services** | **中間ファイル群**<br>・このファイルを元に./openapi/配下のyamlを生成する。| {サービス名}.yaml |
| **src/services/*/paths** | **スキーマー定義ファイル群**<br>・スキーマー定義が記述されている。<br>・中間YAMLから参照される。| {タグ名}/{エンドポイント名}.yaml |
| **src/services/\*/paths/\*/components**| **タグの共通コンポーネント用ファイル群** | {コンポーネント名}.yaml | 
| **src/services/*/examples** | **Exampleファイル群**<br>(アンダースコアやディレクトリを用いると上手く生成されないためハイフンで繋げる) | {タグ名}-{エンドポイント名}-example-{No}.yaml |
| **src/components** | **全体の共通コンポーネント用ファイル群** <br>・ページング情報、バリデーション、認証情報など全社的に共通フォーマットと定義した方がいいようなもの。 | {コンポーネント名}.yaml |
| **scripts** | **スクリプトファイル群**<br>・openapi.yamlやRubyコード生成スクリプトなど。 | 

# 各ファイル記述

各YAMLファイルに記述する内容を順に紹介します。

## 中間ファイル

`./src/services/*.yaml`

- このディレクトリには中間ファイルを配置します。
- 中間ファイルには、サービス概要とエンドポイント一覧のみを記述します。
- (各エンドポイントのスキーマー定義は記述しません。)

| 項目 | 説明 |  |
| --- | --- | --- |
| openapi | 3.0.0 | | 
| info | openapiの基本情報 | |
| servers | テストで使用するサーバー情報を記述する | |
| tags | 各ドメインの概要を記述する | 補足：<br>タグ名＝ドメインとして定義する。 |
| paths | 各エンドポイント一覧を記述する | フォーマット：<br>`$ref: ./paths/{タグ名}/{エンドポイント名}.yaml` |



```src/services/api.yaml
openapi: 3.0.0
info:
  title: XXXX API
  description: "XXXX Service API"
  version: '1.0'
  contact:
    name: XXXX Service API
    url: 'https://xxx..jp'
    email: xxx@xxxxx.xx
  termsOfService: 'https://xxxx.xx/terms'
servers:
  - url: 'http://localhost:3000'
    description: development
  ...
tags:
  - name: animals
    description: 動物
  - name: fruits
    description: 果物
paths:
  # Animals: 動物
  /animals/cats:
    $ref: ./paths/animals/cats.yaml
  /animals/dogs:
    $ref: ./paths/animals/dogs.yaml
  /animals/dogs:
    $ref: ./paths/animals/rabbits.yaml
  /animals/rabbits:
  ...
  # Fruits: 果物
  /fruits/apples:
    $ref: ./paths/resources/apples.yaml
  /fruits/oranges:
    $ref: ./paths/fruits/oranges.yaml
```


## スキーマー定義ファイル

`./src/services/{サービス名}/paths/{タグ名}/*.yaml`

- 各エンドポイントごとにスキーマー定義を記述します。
- operationIdや各オブジェクト名は、コンフリクトを起こさないように、一貫性を持たせます。
- examplesを同じファイルに纏めると、見通しが悪くなるため、refs参照を使い、別ファイルに分けて管理します。

| 項目 | 説明 | フォーマット |
| --- | --- | --- |
| operationId | エンドポイントのユニークID | `{タグ名}_{エンドポイント名}`<br>`{タグ名}_{エンドポイント名}_{メソッド名}`<br>・CURDなど複数メソッドに対応する場合 |
| summary | エンドポイント名のタイトルを記述する | - |
| description | エンドポイント名の詳細仕様を記述する。<br>・なるべく丁寧に詳細に記述する。 | - |
| parameters | リクエストのスキーマーを定義する | $refs名:<br>`{operationId}_Params"`<br> `{operationId}_{オブジェクト名}Params"` |
| responses | レスポンスのスキーマーを定義する | $refs名:<br>・第一階層 = `{operationId}`<br>・第二階層以下 = `{operationId}_{オブジェクト名}`<br>
| responses<br>.examples<br>.example | テストデータのYAMKファイルを指定する | $refs：<br> `{タグ名}-{エンドポイント名}-example-{No}.yaml`l<br>・ハイフンやディレクトリ構成は不可のためハイフンで繋げる。 |
| properties | プロパティ名 | ・ローワーキャメルケースで記述する。(TSの都合上)<br>・user_id → userId |
| requiered | 必須項目 | 必須 |

```./paths/animals/dogs.yaml
get:
  summary: 犬一覧を取得する
  operationId: Animals_DogsGet
  description: |
    xxxxxxxxxxxxxxxx
  tags:
    - animals
  responses:
    "200":
      content:
        application/json:
          schema:
            $ref: Animals_DogsGet
          examples:
            example_1:
              $ref: '../../examples/animals-dogs-get-example-1.yaml'
            example_2:
              $ref: '../../examples/animals-dogs-get-example-2.yaml'
post:
  summary: 犬一覧を取得する
  operationId: Animals_DogsPost
  description: |
    xxxxxxxxxxxxxxxx
  tags:
    - animals
  responses:
    "200":
      content:
        application/json:
          schema:
            $ref: Animals_DogsGet
          examples:
            example_1:
              $ref: '../../examples/animals-dogs-post-example-1.yaml'
components:
  schemas:
    # Dogs Get
    Animals_DogsGet_Params:
      type: object
      properties:
        type:
          type: integer
    Animals_DogsGet:
      type: object
      properties:
        name:
          type: string
        age:
          type: integer
    # Dogs Post
    Animals_DogsPost_Params:
      type: object
      properties:
        name:
          type: string
        age:
          type: integer
    Animals_DogsPost:
      type: object
      properties:
        result: boolean
```

- スキーマーオブジェクトには多様なプロパティがあり、表現の自由度も高いため、フロントエンド・バックエンドで最低限必要な項目のみに絞るようにしています。
  - 参考: [OpenAPI specification : schema-object](https://swagger.io/specification/#schema-object)

| フィールドタイプ名 | 説明 |
| --- | --- | --- |
| type | タイプ | 
| required | 必須 |
| properties.type | プロパティの型 |
| properties.description | プロパティの概要 | 
| properties.nullable |  |
| properties.enum |  |


## 共通コンポーネント

`./src/components/*.yaml`

複数サービスで共通化する必要がある、抽象度の高いオブジェクトをコンポーネントとして記述します。

- ( ファイル名に違和感があるのですが、refs参照の際、ファイル名がそのままオブジェクト名として生成されるため、キャメルケースとしています。)

```Common_Image.yaml
type: object
description: |
  画像オブジェクト
properties:
  src:
    type: string
  alt:
    type: string
required:
  - src
  - alt
```

## Exampleファイル

`./src/services/{サービス名}/examples/*.yaml`

Exampleをスキーマー定義と同一ファイルにおくと、見通しが悪くなるため、examplesディレクトリを区切り管理します。

- (補足として、各スキーマーのexampleフォーマットは、openapi-generatorで、中間ファイルからopenapi.yamlを生成する際に、自動生成されるので、それを用いるとスムーズです。)

```animals-dogs-example-1.yaml
value:
  dogs: [
    {
      id: 1,
      name: taro
    }
  ]
```

# スクリプト例

主要な部分のみ抜粋してます。

## 1. 中間ファイル → openapi.yaml

```root2openapi.sh
service_name=$1
root=${PWD}
src=${root}/src/services/${service_name}
out=${root}/openapi/${service_name}
components=${root}/src/components

docker run --rm \
  -v "${src}:/local/src/" \
  -v "${out}:/local/dist/openapi" \
  -v "${components}:/local/src/components" \
  openapitools/openapi-generator-cli generate \
    -g openapi-yaml \
    -i /local/src/root.yaml \
    -o /local/dist
```

## 2. openapi.yaml →　rubyシリアライザ

```openapi2generator-ruby.sh
service_name=$1
root=${PWD}
src=${root}/openapi/${service_name}/openapi.yaml
out=${root}/dist/openapi2generator-ruby

docker run --rm \
  -v "${src}:/local/openapi.yaml" \
  -v "${out}:/local/dist/openapi2generator-ruby" \
  openapitools/openapi-generator-cli generate \
    -g ruby \
    -i /local/openapi.yaml \
    -o /local/dist/openapi2generator-ruby
```

## 3. Swagger-ui起動

```sh
service_name=$1
root=${PWD}
openapi=${root}/openapi/${service_name}/

docker run \
  -p 80:8080 \
  -e SWAGGER_JSON=/src/openapi.yaml \
  -v `pwd`/openapi/${service_name}:/src swaggerapi/swagger-ui
```

# 余談：REST/RPCについて

本題から少し逸れますが、記事の例文では、わかりやすくするためにREST指向のエンドポイントで記述していますが、実際の運用では、REST／RCPの両方を許容する形で運用しています(既存のAPIがRESTというのもありますが)。ただ、混在させると困惑が生じるため、サービス・タグごとにAPI設計する中で、最適な方を採用するという方針としてます。

REST/RPCに関しては、OpenAPIを色々調査する中で、「OpenAPI（旧Swagger）はREST APIを設計するためのツール」と紹介される記事を多く目にしますが、OpenAPI 3.1.0では、下記のように「REST APIs」の表記が全て「HTTP APIs」と書き換わっていることは着目しておく必要はあるかなと思いました。

[OpenAPI-Specification | OpenAPI supports any type of plain HTTP API](https://github.com/OAI/OpenAPI-Specification/pull/1946)

```
- language-agnostic interface description for REST APIs
+ language-agnostic interface description for HTTP APIs
```

またその中で、stoplightの開発者でもあるphilsturgeon氏が、下記のようにRPCについて言及しており、OpenAPI Initiativeメンバーであるdarrelmiller氏がそれに同意し、v4でのgRPCサポートも示唆されています。

> ● philsturgeon commented on Jun 12, 2019
> Twice in the last few days I have had people ask if its ok to use OpenAPI for RPC, and I would say its better at describing RPC than REST currently. 
> ここ数日で2回、RPCにOpenAPIを使ってもいいかと聞かれたことがあります。現在のところ REST よりも RPC の記述の方が優れていると言っています。

>  Lets remove the limitation by fixing this wording, which would unblock larger talks about things like gRPC support for v4, and maybe even other level 0 implementations like GraphQL.
> この文言を修正することで制限を取り除き、v4のためのgRPCサポートのようなものについての大きな話をブロックしないようにしましょう。


> ● darrelmiller commented on Jun 13, 2019
> I do agree that attempting to associate OpenAPI to REST is no longer doing OpenAPI any favours.
> OpenAPIをRESTに関連づけようとすることは、もはやOpenAPIのためにならないことに同意します。

誤解のないように補足しておくと、ここで言いたいこととしては、REST/RPCのどちらかが優れているのかという話ではなく、サービスの特性に合わせて、最適なAPIを設計をできるように、多くの可能性を選択肢として判断できるようにしておくことが大切だと思いました。


# git. openapi-skeleton

今回紹介させていただいたYAMLファイルやスクリプトと置いてあります。

https://github.com/rkumagai/openapi-skeleton
 
# まとめ

OpenAPIの運用に関しては、まだ導入フェーズということもあり、詰め切れてないこともあり、運用しながら試行錯誤しながらブラッシュアップしていく予定です。また、OpenAPI自体の構成・管理方法よりも、実際にどのようにAPIを設計するのかを考える方が重要で、難しいなと感じています。少しでも参考になれば幸いです。


明日はyamanokuさん記事になります。お楽しみに。