ぼくのかんがえたさいきょうのOpenAPI(Swagger) 仕様の策定環境　その3 docker-composeでのmockserver(Prism) の導入

こちらは前回の記事 [ぼくのかんがえたさいきょうのOpenAPI(Swagger) 仕様の策定環境　その2 docker-composeでのSwagger UI の導入](https://qiita.com/chr/private/c835a80448ca74446b87)、前々回の記事 [ぼくのかんがえたさいきょうのOpenAPI(Swagger) 仕様の策定環境　その1 redocly(yamlのLINT)の導入](https://qiita.com/chr/items/7a21d65cd38091a82adf) の続きとなります。

記事の背景は[前々回の記事](https://qiita.com/chr/items/7a21d65cd38091a82adf)をご参照ください

当記事では、 OpenAPIの定義されたyamlファイルを docker-composeを使ってローカルでPrismを使ってモックサーバーを構築すること、策定したPrismのモックサーバーを Swagger UIからコールできるようにすることを目的としています。

# この記事でわかること
- ~~OpenAPIのyamlファイルのlint方法~~ [前々回の記事](https://qiita.com/chr/items/7a21d65cd38091a82adf)をご参考ください
- ~~OpenAPIのyamlファイルへ独自lint(カスタムルール)の追加方法~~ [前々回の記事](https://qiita.com/chr/items/7a21d65cd38091a82adf)をご参考ください
- ~~docker-composeを使って swagger uiの構築方法と複数ファイルAPI定義があった場合のdocker-composeの実装方法~~ [前回の記事](https://qiita.com/chr/private/c835a80448ca74446b87)をご参考ください
- docker-composeを使って OpenAPIの定義からmockserverを起動する方法と ホットリロードの実装方法

---
皆さんはじめまして株式会社クライドでオフショア開発部門の事業部長とフィリピン支社のCEOをしている Matsuo です。

今回、私が取り組んでいるプロジェクトにて、FrontendはNextJS , BackendはJavaで多くのAPIを開発する必要があったため、開発をよりスムーズにし、Frontendはバックエンドの実装を待たずにAPIをコールできるようにするのと、FrontendとBackendでAPIの形式の認識齟齬と実装上の違いが起きないようにするために、OpenAPIの定義を使ってFrontendとBackendで共通のAPI仕様を使って開発できるようにする実装方針を選定しました。
そこで、Swagger UIや LINT、Mockserverなどいくつか構築に手間取った箇所があったため、共有します。

# 前提
複数ファイルのOpenAPIの定義ファイルを持っており、複数の定義に合わせて動作可能なモックサーバーを構築します。
モックサーバーが必要な背景としては、プロジェクトのAPI数が多く、フロントエンドのエンジニアがバックエンドの実装完了を待たずに実装できるようにするためです。
また、フロントエンドエンジニアはOpenapiの定義を担保したモックサーバーをコールすることによって、バックエンドで仕様と実装の違いが生まれてしまう可能性を考慮せずに、教科書のみを信じて実装を進めることができます。
モックサーバーの候補はいくつかありましたが、今回は [Prism](https://stoplight.io/open-source/prism) を使ってAPIサーバーを構築しています。


# プロジェクトのファイル環境
私のプロジェクトでは以下ファイル構成で環境を構築しています

```tree
.
├── infra_structure
│   ├── Caddyfile
│   ├── CustomPrism.Dockerfile
│   ├── docker-compose.yaml
│   └── start-prism.sh
├── node_modules
├── openapi
│   ├── hoge-api.yaml
│   ├── common
│   │   ├── error-response.yaml
│   │   ├── parameters.yaml
│   │   ├── request-bodies.yaml
│   │   └── schema.yaml
│   ├── fuga-api.yaml
├── package-lock.json
├── package.json
├── plugins
│   ├── custom-lint-rules.js
│   └── rules
│       ├── enum-snake-case.js
│       ├── operation-id-camel-case.js
│       ├── query-param-snake-case.js
│       ├── schema-property-snake-case.js
│       ├── tags-kebab-case.js
│       └── tags-kebab-case.js.gz
├── readme.md
└── redocly.yaml
```

openapi/hoge-api.yamlと、openapi/fuga-api.yamlがメインのOpenAPIの定義ファイルとなり、それぞれがcommon配下のyamlを読み込む構成となっています。

dockerの構成に関するファイルは、 infra_structure配下へ設置しています。

# infra_structure/docker-compose.yaml の定義内容
```yaml:infra_structure/docker-compose.yaml
version: "3"
services:
  proxy:
    image: caddy
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
    ports:
      - "8180:80"
    depends_on:
      - hoge_api
      - fuga_api
      - swagger_ui

  swagger_ui:
    image: swaggerapi/swagger-ui
    volumes:
      - ../openapi:/usr/share/nginx/html/api
    ports:
      - "8181:8080"
    environment:
      URLS:
      >
        [
          {
            url: "api/hoge-api.yaml",
            name: "hoge-api",
          },
          {
            url: "api/fuga-api.yaml",
            name: "fuga-api",
          },
        ]
      PORT: 8080

  hoge_api:
    build:
      context: .
      dockerfile: CustomPrism.Dockerfile
    volumes:
      - ../openapi:/openapi
    command: ["mock -d -p 4010 --host 0.0.0.0 /openapi/hoge-api.yaml"]

  fuga_api:
    build:
      context: .
      dockerfile: CustomPrism.Dockerfile
    volumes:
      - ../openapi:/openapi
    command: ["mock -d -p 4010 --host 0.0.0.0 /openapi/fuga-api.yaml"]

```

※当yamlに出てくるswagger_ui については[前回の記事](https://qiita.com/chr/private/c835a80448ca74446b87)を参照してください

# 起動コマンド
```sh
cd infra_structure
docker-compose up
```

# モックサーバーへのリクエスト
ポート番号：8180を以下箇所にて割り当てているため、URLは `http://localhost:8180/` がベースとなります。
caddyを使い、内部のDockerファイルへproxyしている構成となります。
```yaml:infra_structure/docker-compose.yaml
    ports:
      - "8180:80"
```

# 設定のポイント
## Cadyfileの設定
Caddyについては公式をご参考ください、dockerでよく使うnginx-proxyと類似した挙動をするサービスとなります。
https://caddyserver.com/

当プロジェクトでの設定しているCadyfileは以下のようにしています
```go:infra_structure/Caddyfile
http://localhost
route /hoge-api/* {
    uri strip_prefix /hoge-api
    reverse_proxy hoge_api:4010
    }
    
route /fuga-api/* {
    uri strip_prefix /fuga-admin
    reverse_proxy fuga_api:4010
    }
```
このように localhostのURLをベースに /hoge-api/ へのアクセスは、 hoge_apiコンテナの4010ポートへ、
/fuga-api/ へのアクセスは fuga_apiコンテナの4010ポートへproxyするようにしています。
尚、 `uri strip_prefix /fuga-admin` の記述によって、URLに入力されている `/fuga-admin` を取り除いて、実際のAPIが動作するコンテナへproxyするようになっています。

## 各OpenAPIのyaml毎にDockerコンテナを起動するようにする
```yaml:infra_structure/docker-compose.yaml
  hoge_api:
    build:
      context: .
      dockerfile: CustomPrism.Dockerfile
    volumes:
      - ../openapi:/openapi
    command: ["mock -d -p 4010 --host 0.0.0.0 /openapi/hoge-api.yaml"]

  fuga_api:
    build:
      context: .
      dockerfile: CustomPrism.Dockerfile
    volumes:
      - ../openapi:/openapi
    command: ["mock -d -p 4010 --host 0.0.0.0 /openapi/fuga-api.yaml"]

```
こちらの処理で 各APIのyaml毎に Dockerコンテナを起動するように設定しています。

当構成では、Prismを採用しており、Prismは[元々のDockerfile](https://docs.stoplight.io/docs/prism/f51bcc80a02db-installation#docker) がありますが、
Prismの [Dynamic Response Generation with Faker](https://docs.stoplight.io/docs/prism/9528b5a8272c0-dynamic-response-generation-with-faker) 機能を使って  **動的にmockserverのレスポンスを制御したかったため、モックサーバーで動作を確認しながら実装できるようにするために、
yamlの定義を更新したらホットリロードをできる Dockerイメージを作成するようにしました。**

ぜひ本家の Dockerimageで今後対応してほしいところです。

## ホットリロード対応の Docker作成
ホットリロード対応のDockerは以下ファイル構成で作成しています
```tree
.
├── infra_structure
│   ├── CustomPrism.Dockerfile
│   └── start-prism.sh
```

各ファイルの中身は以下のようになっています
```Dockerfile: infra_structure/CustomPrism.Dockerfile
################
# PrismのDockerfileは、hotload機能がないため、Hotloadを実装する
# Dockerfile単体での実行サンプル（事前に custom-prismとしてbuildしている必要があります)
# docker run --rm -it -p 4010:4010 -v $PWD:/tmp custom-prism "mock -d -p 4010 /tmp/openapi/hoge-api.yaml"
################

# Dockerfile
FROM stoplight/prism:4

# nodemonのインストール
RUN npm install -g nodemon
RUN chmod +x /usr/src/prism/packages/cli/dist/index.js

# start-prism.sh スクリプトをコピー
COPY ./start-prism.sh /start-prism.sh
RUN chmod +x /start-prism.sh

# デフォルトコマンドの設定
ENTRYPOINT ["nodemon", "--watch", "/openapi/", "--ext", "yaml,yml", "--exec", "sh /start-prism.sh"]

```
nodaemonのファイル監視の対象ディレクトリを ~~/tmp/~~ /openapi/ に固定されてしまっているのはあまり関心できる実装ではありませんが、ここでは目をつぶっています。 (2023/12/21 追記, /tmpへ固定すると、prismが生成した一時ファイルがローカルへ反映してしまうためパスを変更) 

内部で使用している start-prism.shは以下のようにしています。
```sh: infra_structure/start-prism.sh
#!/bin/sh
# start-prism.sh

# 全ての引数を一つの文字列として受け取る
PRISM_COMMAND="prism $@"

# コマンドを評価して実行
eval $PRISM_COMMAND
```

Dockerファイルでは、 prismのImageをベースに、nodemonをインストールし、/tmp/配下のyaml,ymlファイルの変更監視しています。
変更されると、 `sh /start-prism.sh` にて prismが再起動されるようになっています。

start-prism.sh では コマンドライン引数にパラメータが文字列として渡され、それを 文字列で prismコマンドと結合し、evalすることでPrismの実行をしています。

この流れを行うことにより、本来の Dockerfileである `stoplight/prism:4` とほぼコールの仕方を変更せずに、ホットリロードに対応することができるようになりました。

docker-compose.yamlでは `stoplight/prism:4` のイメージを使うかわりに、上述の独自のDockerfileを使用するようにしています。
docker-composeのcommandの箇所で、最終的にprismに渡したい引数を定義しています。
引数を外部から受け渡して実行できるようにすることで、Prismのモックサーバー以外の機能や、モックサーバーへ渡したいパラメーターをDockerfileを更新せずに変更できるようにしています。

``` yaml:infra_structure/docker-compose.yaml
    command: ["mock -d -p 4010 --host 0.0.0.0 /tmp/fuga-api.yaml"]
```
モックデータではなくsampleデータで返却させたい場合は、 `-d` のオプションを削除することで動作を変更できます。

# Prismでモックデータを返却出力し出力内容を制御できるようにする
モックデータの出力は、Prismの起動コマンドにて `-d` オプションをつけることで [**Dynamic Response Generation with Faker**](https://docs.stoplight.io/docs/prism/9528b5a8272c0-dynamic-response-generation-with-faker) の機能が有効になります。

具体的には、x-fakerパラメーターをOpenapiの各yamlの定義(例えばSchema)の各フィールドへ追加することで、生成されるモックデータをコントロールすることが出来ます。

これは、[faker-js](https://github.com/faker-js/faker) の機能を執筆時点では使用しています。
使用方法は以下のように `x-faker` のパラメーターを設定します
``` yaml: example
Pet:
  type: object
  properties:
    id:
      type: integer
      format: int64
    name:
      type: string
      x-faker: name.firstName
      example: doggie
    photoUrls:
      type: array
      items:
        type: string
        x-faker: image.imageUrl
```

これらの `name.firstName` や `image.imageUrl` は、 fakerオブジェクトのmethod名を指しています。
尚、当記事執筆時点（2023/12/20）では、 `stoplight/prism:4` は faker-jsの [version:6](https://github.com/stoplightio/prism/blob/v4.14.1/package.json#L31) に依存しているため、
最新のfaker-js(執筆時点ではversion:8) のドキュメントは参考になりませんでした。
[Version6のドキュメント](https://v6.fakerjs.dev/)を参照する必要があります（オブジェクトグループ名がversion8と異なります）

## x-fakerのパラメーター定義
yaml上に記載する x-fakerパラメーターは、faker-jsのmethod名の指定以外に、methodへ引き渡したいパラメーターを設定することができます。

### 配列でのパラメーター定義
例えばfirstNameを生成したい場合、
```yaml:example
Person:
  type: object
  properties:
    first_name:
      type: string
      x-faker: name.firstName
      example: yuki
```
とする以外に、
```yaml:example
Person:
  type: object
  properties:
    first_name:
      type: string
      x-faker: 
          name.firstName:
              - male
      example: yuki
```
として配列でパラメーターを設定することで `faker.name.firstName("male")` と同じ実行結果を得ることができます。
name.firstNameについての仕様は[こちら](https://v6.fakerjs.dev/api/name.html#firstName)です。

## オブジェクトでのパラメーター定義
[datatype.number](https://v6.fakerjs.dev/api/datatype.html#number) のように引数がオブジェクトとなっている場合は、以下のようにすることで引数を設定することができます。
```yaml: example
Person:
  type: object
  properties:
    age:
      type: integer
      x-faker:
          datatype.number:
              min: 100
              max: 250
      example: 40
```

## faker-jsの日本語化 / Prismの挙動変更
faker-jsでは日本語化機能を持っています。
Prism経由でfaker-jsを使用して言語を切り替える場合は、 yamlへ定義を追加します。
また、Prismがarrayオブジェクトなどで自動で生成するデータ個数もyaml上で定義することができます。
設定できるオプションは[こちら](https://docs.stoplight.io/docs/prism/9528b5a8272c0-dynamic-response-generation-with-faker#configure-json-schema-faker)で確認することができます。
```yaml:openapi/hoge-api.yaml
x-json-schema-faker:
  locale: ja
  min-items: 3
  max-items: 10
  resolve-json-path: true
```

## 日本語化した場合にemailアドレスの@マークより前が日本語になってしまう
日本語化を実装して、emailアドレスを fakerの [internet.email](https://v6.fakerjs.dev/api/internet.html#email)を使ったところ、@より前が日本語で出力されてしまいました。
例：`山田花子_53@gmail.com` これでは、frontへ組み込んだときに意図しないバリデーションエラーが発生してしまうおそれがあります。
そのため、以下のようにfistNameとlastNameのパラメーターを付与することでこの課題を解決することができました。
```yaml:example
    email:
      type: string
      format: mailaddress
      x-faker: 
        internet.email: 
          - "test"
          - "last-name"
      example: example@gmail.com
```

# 同じくDockerで立てたSwagger UIからMockserverをUI上でコールできるようにする
Swagger UIでは、yamlのserversプロパティを設定することで、Swagger UIからリクエストできるサーバーを選択することができます。
```yaml:openapi/hoge-api.yaml
servers:
  - url: '{ApiBaseURL}/hoge-api'
    variables:
      ApiBaseURL:
        enum: 
          - https://api.admin.example.com
          - "http://localhost:8180"
        default: "http://localhost:8180"
        description: Your server host
```
こちらを設定することで、以下画像のようにリクエスト先サーバーを選択できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69167/3025fc07-4855-c5ca-8571-543b05140109.png)

この設定をすることで、Swagger UIの 各APIの **Try it out** ボタンより、モックサーバーへ実際にリクエストを送信できるようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69167/fff66c60-0476-27a8-f01c-d1b1c3d7a623.png)


---
以上がdocker-composeで mockserverを立て、実際のPrismのパラメーターを制御しモックサーバーとして使用可能な状態にした内容となります。

OpenAPIで定義を作成して実装を進めていくようなスキーマ駆動開発を進めようとされている方や、スキーマ駆動開発に向けたスキーマ定義環境の環境構築をされているような方に当3記事が参考になれば幸いです。

* [ぼくのかんがえたさいきょうのOpenAPI(Swagger) 仕様の策定環境　その1 redocly(yamlのLINT)の導入](https://qiita.com/chr/items/7a21d65cd38091a82adf)
* [ぼくのかんがえたさいきょうのOpenAPI(Swagger) 仕様の策定環境　その2 docker-composeでのSwagger UI の導入](https://qiita.com/chr/items/c835a80448ca74446b87)
* ぼくのかんがえたさいきょうのOpenAPI(Swagger) 仕様の策定環境　その3 docker-composeでのmockserver(Prism) の導入(当記事)
---
次回、openApiのyamlからNextJSで利用できるAPI Clientをtypescriptで自動生成できるようにした を執筆予定です。