OpenAPIの運用コスト下げたい。

ケイ

2023.03.15

168

ケイです。


突然ですが皆さん、RestAPIを開発する際はどのように仕様書を管理しているでしょうか?


色々な方法があるかと思いますが、僕はOpenAPIを好んで採用しています。

細かい仕様まで書き込めて、それをコードベースで管理できる点は有難いです。

レンダリングUIも色々選択肢があり、Swaggerを使えばモックとして使えてしまうのも重宝されるポイントではないでしょうか?


今回はそんなOpenAPIを書いていく中で、僕がよく使う方法の一つをご紹介します。

tl;dr

OpenAPIをSwaggerとかRedocでブラウザに表示させながら、

書き換えもしたいし、

リアルタイムに変更を見たいし、

さらにはHTMLを書き出したい時だってあるよね。


そんな時はこの構成でホットリロードとバンドルをしちゃえば運用コスト低めで叶えることができるよね。


OpenAPIに対する悩み

ゼロから書いていくのは割と辛い。

はい、いきなり愚痴が出てしまいました。


そうなんです、OpenAPIをゼロから書く時って割とファイル量が多く、着手から10分後くらいには途方もなさを感じてメンブレ必至です。


既存APIの実装からOpenAPI形式で仕様を書き出すツール等ありますが、初期整備として仕様書を書く際はゼロから書かなければいけないケースが大半かと思います。


前述していますが、OpenAPIは細かい仕様を書くことができる。

だからこそ、完成度の高い仕様書を作成するとなると必然的にファイル量は増えてしまいます。

ちょっと修正したい時だってある。

何かしらの仕様に変更や誤りがあったり、単純に誤字していることに気が付いたり。


いざ実装を進めていると、細かいことにも気が付くようになり、多少なり仕様書を書き換えることがあります。


「仕様書を見ていつつも、書き換えもしたい」

そんなニーズです。

ファイル分割は絶対にしたい。でもバンドルもしたい。

これはOpenAPIの強みの一つなので、僕にとってファイル分割は必ず採用したいものです。

(というより、OpenAPIを一つのファイルで書ききるのは鬼の所業…。)


そして、分割されたファイルを一つにバンドルしておきたい

一つのファイルにまとまっていると、何かと便利です。


例えば、

  • ファイルのPath指定による$refはSwaggerだと上手くいくが、Redocだと上手く参照できずエラーになる。
  • OpenAPIをHTMLにしてチーム内で見れるようにホスティングしたいが、分割されていると上手くHTML出力ができない。

みたいなケース。

(使っているツールに依る部分もあるかと思います。)


こんなケースをカバーするためにも、一つのファイルになっていると恩恵を受けることがあります。

この悩みに対処しよう

前置きが長くなってしまいました。

さて、前述のお悩み解決に対処する方法です。


ポイントはズバリ二つ。

  1. ホットリロード
  2. 分割されたファイルのバンドル

Dockerでレンダリング & ホットリロード & バンドル

以下の内容で構成していきます。

今回はDockerでRedocとして表示させるので、Redocの公式イメージを使います。


ディレクトリ構成

/
├── compose.yaml  # Docker Compose
├── docker
│   └── redoc
│       ├── Dockerfile
│       ├── bundle.ts  # バンドラー
│       └── start.sh  # Docker EntryPoint
└── api_docs/  # 分割されまくったOpenAPIたち
       ├── components/
       │   ├── index.yaml
       │   ├── parameters/
       │   │   ├── index.yaml
       │   │   ├── path/
       │   │   └── query/
       │   ├── requestBodies/
       │   │   ├── index.yaml
       │   │   └── xxx/
       │   ├── responses/
       │   │   ├── index.yaml
       │   │   └── xxx/
       │   ├── schemas/
       │   │   ├── xxx.yaml
       │   │   └── index.yaml
       │   └── securitySchemes/
       │       ├── xxx.yaml
       │       └── index.yaml
       ├── paths/
       │   ├── index.yaml
       │   └── xxx/
       │── tags
       │   └── xxx.yaml
       ├── index.yaml
       └── openapi.yaml  # バンドルされたOpenAPI

compose.yaml

version: "3.8"

	services:
	  redoc:
	    build:
	      context: .
	      dockerfile: ./docker/redoc/Dockerfile
	    volumes:
	      - ./docker/redoc/bundle.ts:/app/bundle.ts
	      - ./docker/redoc/start.sh:/app/start.sh
	      - ./api_docs:/usr/share/nginx/html/docs  # Redoc公式イメージでは '/usr/share/nginx/html/' 配下にOpenAPIを置く必要がある
	    ports:
	      - 9000:80

docker/redoc/Dockerfile

# Redoc公式イメージをベースに
FROM redocly/redoc:v2.0.0
	
WORKDIR /app
	
# ホットリロードにnpmパッケージを使用するためnpmインストール
RUN apk add --no-cache npm
	
# npmパッケージをコンテナ内でしようするための設定
ARG NODE_MODULE_PATH=/node
RUN npm config set prefix=${NODE_MODULE_PATH}
ENV PATH $PATH:${NODE_MODULE_PATH}/bin
ENV NODE_PATH ${NODE_MODULE_PATH}/lib/node_modules
	
# レンダリングするOpenAPIのPath (Redoc公式イメージの環境変数)
# ファイル分割された '$ref' が含まれていると上手く参照できずエラーになる
# そのため、バンドル前の  'index.yaml' は使わず、バンドルされたファイルを指定する
ENV SPEC_URL docs/openapi.yaml
	
# ホットリロードに使用するnpmパッケージ群をインストール
RUN npm i -g \
    forever \
	ts-node \
	chokidar \
	swagger-merger
	
# EntoryPointにShellスクリプト指定
ENTRYPOINT [ "sh", "start.sh" ]

docker/redoc/start.sh

# RedocのNginxを立ち上げる
# 'docker/redoc/Dockerfile''ENTRYPOINT' で公式イメージの ' ENTRYPOINT ' は上書きされてしまったので再定義
# '&' を付けてバックグラウンドプロセスにして、後述のバンドラーのプロセスと並走できるようにする
source /usr/local/bin/docker-run.sh &
	
# バンドラー実行
ts-node bundle.ts

docker/redoc/bundle.ts

// 'ts-ignore' 連発。非常に良くないので反面教師で。。。
// @ts-ignore
const chokidar = require("chokidar")  // ウォッチャー
// @ts-ignore
const swaggerMerger = require("swagger-merger")  // バンドラー

const rootPath = "/usr/share/nginx/html/docs/"  // OpenAPIが置かれているPath
const rootFile = `${rootPath}index.yml`  // OpenAPIのルートファイルPath
const mergedFile = `${rootPath}openapi.yml`  // バンドルしたOpenAPIを吐き出すPath
	
chokidar
  .watch(rootPath, { ignored: mergedFile })
  .on("all", (event: string, path: string) => {

    // ウォッチャーの検知したイベントをログに出力
    console.log(event, path)

    // OpenAPIをバンドル
    swaggerMerger({ input: rootFile, output: mergedFile })
        .then(() => {
      	    console.log("ReBundled OpenAPI")
        })
    	.catch((err: object) => {
	        console.error(err)
	    })
	})

これで何が起こるか (まとめ)

  • レンダリング
    • Dockerを立ち上げればRedocをブラウザで確認することができます。
    • API実装中には必須だと思います。
  • ホットリロード
    • OpenAPIを書き換えたらRedocもそのタイミングで更新されます。
      • 表示を確認しながらサクサク書くことができます。
    • 「変な表示になってないかな?」「API実装中だけどちょっと修正しとこ。」ができます。
  • バンドル
    • OpenAPIを書き換えたら一つのOpenAPIとしても書き出されます。
    • これさえあれば色んな用途で柔軟に使い回せます。

終わりに

今回はOpenAPIにまつわる開発体験の向上を軸にご紹介しました。

割と良くある構成なのかな?とも思いますし、ツールの選定によってはもっとシンプルにすることができるかと思います。


もっと良い構成をご紹介できるように、チャンスがあれば色々試していきたいと思います。


最後まで読んでくださりありがとうございました!

では!ノシ

この記事をシェアする