Swaggerを使ってAPIドキュメントとモックを自動更新する

y.kimura

はじめに

プロダクトエンジニアリング部の木村です。

今回はOpenAPI 3.0形式で書いたAPIドキュメントをSwaggerのDockerコンテナを利用して APIドキュメントサーバ及びモックサーバとしてローカルで立ち上げる方法を紹介します。

編集がリアルタイムに反映されるため、ローカルで立ち上げてプロダクトのコードを書きながらAPIドキュメントを修正、修正した内容を元にさらにコーディングを進める、といった使い方ができるかもしれません。

APIドキュメントのソースファイルをGitリポジトリなどで管理し、今回の紹介するDockerイメージを社内のサーバで立ち上げソースファイルを定期的にPullしておけば、自動的に最新のドキュメントが反映されるAPIドキュメント兼モックサーバを共有することができます。

実現までの流れ

  1. APIドキュメントとしてOpenAPI 3.0形式でyamlファイルを作成・更新する
  2. 作成したyamlファイルに対してDockerコンテナでSwaggerのコードジェネレーターを実行してnode.js用ソースコードを生成する
    • ソースコードを生成するディレクトリをホスト側でマウントしておく
  3. 別のDockerコンテナで、2で生成されたソースコードを元にnode.jsサーバを立ち上げる
    • 2でマウントしたディレクトリをこちらのコンテナでもマウントする
  4. APIドキュメントサーバ、モックサーバとして利用する

1で編集するyamlファイルは、Dockerコンテナを動かすサーバに置く必要があります。サーバ上で直接編集するか、手元のマシンで編集したファイルをsambaやsftpなどで共有してください。

実行環境

docker-composeが実行できる環境が必要です。docker-composeのインストールについてはこちらを参考にしてください。

※ 注意

モックサーバとして使用する場合、レスポンスはコンポーネントを参照する形で記述する必要があります。API定義に直接exampleを記述してもモックのレスポンスが返ってきません。

▼ swaggerファイルの記述例 (クリックで開きます)

ファイル・ディレクトリ構成

ファイル・ディレクトリ構成は以下となります。各ファイルの詳細は後述します。

documents/
  └ todo.yml  <-- APIドキュメントファイル
mount/
  └ todo/     <-- ソース生成コンテナによって作成される、nodejsのソースファイルが生成される
patches/
  └ oas3-tools+2.2.3.patch  <-- cors対応とdeprecatedなライブラリの削除用パッチ
docker-compose.yml
Dockerfile
Dockerfile_generator
wathc.sh

Dockerfile・docker-composeファイル

コードを生成するコンテナとAPIドキュメント兼モックサーバのコンテナのDockerfileは次のようになります。

コード生成コンテナ Dockerfile_generator

FROM swaggerapi/swagger-codegen-cli-v3

RUN apk update && apk add --no-cache bash

スクリプトを実行するためにbashをインストールします。

ドキュメントサーバ Dockerfile

FROM node:18-alpine

RUN apk update && apk add --no-cache git

WORKDIR /home/node

RUN npm config set cache '/home/node/.npm'

RUN npm install -g npm@9.6.2

docker-compose.yml


version: '3.2'

services:
  todo-generator:
    build:
      context: "."
      dockerfile: "./Dockerfile_generator"
    entrypoint: ["./watch.sh",  "/files/"]
    volumes:
      - ./watch.sh:/watch.sh
      - ./documents/:/files:ro
      - ./mount:/swagger

  todo-server:
    build:
      context: "."
      dockerfile: "./Dockerfile"
    ports:
        - "8080:8080"
    volumes:
      - ./mount/todo:/home/node:rw
      - ./patches/oas3-tools+2.2.3.patch:/home/node/patches/oas3-tools+2.2.3.patch:rw
    command: npm start
    environment:
        - NODE_PATH=/usr/local/lib/node_modules

コード生成コンテナでは以下をマウントします

  • 自動更新用のスクリプト
    • APIドキュメントファイルが更新されたら新しいサーバ用ソースファイルを生成する
  • APIドキュメントファイルのディレクトリ
  • 生成したサーバ用ソースファイルを出力するためのディレクトリ

ドキュメントサーバコンテナでは以下をマウントします

  • サーバ用ソースファイルを出力するためのディレクトリ
  • サーバ用ソースファイル用のパッチファイル

APIドキュメントが複数ある場合は、ドキュメントサーバをAPIドキュメントの数だけ起動する必要があります。その際、コンテナのサービス名とマウントするサーバ用ソースファイルを出力するためのディレクトリをそれぞれ別のものにします。

例として、APIドキュメントがtodo.yml, operation.yml, management.ymlと3つある場合は以下のようになります。

...
todo-server:
...
    ports:
        - "8080:8080"
    volumes:
      - ./mount/todo:/home/node:rw
      - ./patches/oas3-tools+2.2.3.patch:/home/node/patches/oas3-tools+2.2.3.patch:rw
    command: npm start
...

operation-server:
...
    ports:
        - "8081:8080"
    volumes:
      - ./mount/operation:/home/node:rw
      - ./patches/oas3-tools+2.2.3.patch:/home/node/patches/oas3-tools+2.2.3.patch:rw
    command: npm start
...

management-server:
...
    ports:
        - "8082:8080"
    volumes:
      - ./mount/management:/home/node:rw
      - ./patches/oas3-tools+2.2.3.patch:/home/node/patches/oas3-tools+2.2.3.patch:rw
    command: npm start

パッチファイル

SPA等でモックサーバを使う場合、AjaxでAPIにアクセスするのが一般的ですが、swaggerのモック機能がCORSのプリフライトリクエストに対応していないため、パッチを当てて強引に対応しています。

また、サーバ起動時にdeprecatedな警告がでるので、そちらも併せて修正しています。

パッチファイル patches/oas3-tools+2.2.3.patch

diff --git a/node_modules/oas3-tools/dist/middleware/express.app.config.js b/node_modules/oas3-tools/dist/middleware/express.app.config.js
index e11dddb..056e59c 100644
--- a/node_modules/oas3-tools/dist/middleware/express.app.config.js
+++ b/node_modules/oas3-tools/dist/middleware/express.app.config.js
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
 exports.ExpressAppConfig = void 0;
 const express = require("express");
 const cookieParser = require("cookie-parser");
-const bodyParser = require("body-parser");
+const cors = require("cors");
 const swagger_ui_1 = require("./swagger.ui");
 const swagger_router_1 = require("./swagger.router");
 const swagger_parameters_1 = require("./swagger.parameters");
@@ -19,9 +19,8 @@ class ExpressAppConfig {
         this.app = express();
         const spec = fs.readFileSync(definitionPath, 'utf8');
         const swaggerDoc = jsyaml.safeLoad(spec);
-        this.app.use(bodyParser.urlencoded());
-        this.app.use(bodyParser.text());
-        this.app.use(bodyParser.json());
+        this.app.use(cors());
+        this.app.use(express.text());
         this.app.use(this.configureLogger(appOptions.logging));
         this.app.use(express.json());
         this.app.use(express.urlencoded({ extended: false }));

自動更新スクリプト

APIドキュメントファイルが更新されるたびにサーバ用ソースファイルを生成するコマンドを実行します。

コマンドを実行後に生成されたpackage.jsonを書き換えています。

自動更新スクリプト watch.sh

#!/bin/bash
if [ $# -ne 1 ]; then
        exit 1
fi

command="java -jar /opt/swagger-codegen-cli/swagger-codegen-cli.jar generate -l nodejs-server -i ##FILE## -o /swagger/##DIR##/"

function generateSource() {
    file=$1

    # ファイル名から拡張子を除いたディレクトリにソースが生成される
    dir=$(echo $file | sed -r 's|\/files\/(.*)\.yml|\1|')
    cmd=$(echo $command | sed -e "s|##FILE##|$file|")
    cmd=$(echo $cmd | sed -e "s|##DIR##|$dir|")
    if [ ! -d "/swagger/${dir}" ]; then
        mkdir "/swagger/${dir}"
    fi

    echo $cmd
    eval $cmd

    # package.jsonを書き換える
    #   * npm installで必要なライブラリをインストール
    #   * ファイルが更新されるたびに再起動するようにnodemonで起動
    #   * patch-packageでパッチをあてる
    sed -i 's|"prestart": "npm install"|"prestart": "npm install --cache /home/node/.npm cors patch-package --save \&\& npm install --cache /home/node/.npm"|' /swagger/${dir}/package.json
    sed -i 's|"start": "node index.js"|"start": "node index.js",\n        "postinstall":"patch-package"|' /swagger/${dir}/package.json
    sed -i 's|"node index.js"|" node /home/node/node_modules/nodemon/bin/nodemon.js index.js"|' /swagger/${dir}/package.json
}


arr=()
echo "監視対象ディレクトリ $1"
INTERVAL=10
declare -A last=()

# 複数のswaggerファイルに対応
# 起動時に一度ジェネレーターを起動してからswaggerファイルの最終更新日時を保持する
for file in $(find "$1" -maxdepth 1 -type f); do
    echo "watching: $file"
    arr+=($file)
    last["$file"]=`ls --full-time $file | awk '{print $6"-"$7}'`
    generateSource $file
done

# ファイルの最終更新日時が新しい場合はジェネレーターを起動
while true; do
        sleep $INTERVAL
        for file in "${arr[@]}"; do
            current=`ls --full-time $file | awk '{print $6"-"$7}'`
            _last=${last[$file]}
            if [ $_last != $current ] ; then
                    echo ""
                    echo "updated: $current"
                    last[$file]=$current
                    generateSource $file
            fi
        done
done

起動

docker-composeを起動します。

初回起動時はドキュメントサーバ用のソースが生成が間に合わないため、ドキュメントサーバの起動(npmの実行)が失敗します。そのような場合はdocker-composeを再起動するか、docker restartなどでドキュメントサーバコンテナを起動してください。

ドキュメントサーバにはブラウザで http://{ホスト名}:{ポート}/docs/ にアクセスしてください。

モックサーバは http://{ホスト名}:{ポート}/{APIのパス} にアクセスしてください

ドキュメント内でパス、クエリ、ボディのパラメーターに必須項目が設定されている場合は、パラメーターに不備があるとエラーレスポンスが返ってきます。

todo$ curl http://localhost:9281/v1/todo
[
  {
     "id": 1,
     "title": "日次タスクの実行",
     "detail": "日次タスクを実行する"
  },
  {
     "id": 2,
     "title": "開発",
     "detail": "設計・コーディング・テスト"
  }
todo$

最後に

Swaggerの機能でモックサーバとしても使えますが、リクエストの内容に応じてレスポンスを変えるといったことはできません。そのような機能が必要な場合はWiremockなどのモック専用ライブラリを使いましょう。

APIドキュメントに限らずドキュメントの類は、必要性が無い場合は実態が変わっても一度書いたらそのままメンテナンスされずに放置されるということも少なくありません。

ドキュメントを書いた結果をダイレクトに開発に利用できたり、社内で共有できる環境を作ることでメンテナンスし続けるモチベーションを上げることにつながっていくかもしません。

おまけ

OpenAPI 3.0ドキュメントの作成には、VSCodeとエクステンションの「OpenAPI (Swagger) Editor」が便利です。こちらを使えばプレビューを見ながらAPIドキュメントを書けるためストレスなくドキュメントを作成することができます。

エクステンションをインストール後、yamlファイル編集中に右上の虫眼鏡付きアイコンをクリックするとリアルタイムプレビューが見られます。

y.kimura

Posted by y.kimura